Updated on 2026-08-14
This commit is contained in:
commit
06a915a0f4
2186 changed files with 101417 additions and 36593 deletions
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.
|
||||
|
|
@ -63,21 +63,32 @@ tasks.withType<Detekt>().configureEach {
|
|||
exclude { it.file.absolutePath.contains("/build/generated/") }
|
||||
}
|
||||
dependencies {
|
||||
/** DI */
|
||||
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Local storages */
|
||||
// region Kotlin
|
||||
implementation(deps.kotlin.coroutines)
|
||||
// endregion
|
||||
|
||||
// region AndroidX
|
||||
implementation(deps.androidx.annotation)
|
||||
implementation(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
||||
/** Other libraries */
|
||||
// region Other libraries
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
// endregion
|
||||
|
||||
/** Core modules */
|
||||
// region Core modules
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,4 +1,12 @@
|
|||
[
|
||||
{
|
||||
"name": "AND_15901_STORIES_CONTAINER_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1322_FORCE_UPDATE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -8,49 +16,13 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_ETH_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "APP_REDESIGN_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_APPROVAL_ENABLED",
|
||||
"version": "5.37"
|
||||
},
|
||||
{
|
||||
"name": "DYNAMIC_ADDRESSES_ENABLED",
|
||||
"version": "5.39"
|
||||
"name": "TWI_485_USEDESK_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ASSETS_DISCOVERY_ENABLED",
|
||||
"version": "5.38"
|
||||
},
|
||||
{
|
||||
"name": "SOLANA_TX_HISTORY_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "SOLANA_SCALED_UI_AMOUNT_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "HEDERA_ERC20_ENABLED",
|
||||
"version": "5.37"
|
||||
},
|
||||
{
|
||||
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"version": "5.38"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -63,61 +35,13 @@
|
|||
"name": "ADDRESS_SYNC_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15120_SWAP_INTEGRATED_APPROVE",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_AB_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1403_PUSH_NOTIFICATION_SETTINGS_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15310_ADD_FUNDS_STAGE1",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1377_MANAGE_FUNDS",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15402_ADI_MAIN_SCREEN_DEFAULT_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15103_SWAP_RATE_EXPERIENCE_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED",
|
||||
"version": "5.39"
|
||||
},
|
||||
{
|
||||
"name": "AND_15154_YIELD_PROMO_ENABLED",
|
||||
"version": "5.39.2"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED",
|
||||
"version": "5.39"
|
||||
"name": "TWI_1403_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "AND_15438_BACKEND_AUTHENTICATION_ENABLED",
|
||||
|
|
@ -125,40 +49,24 @@
|
|||
},
|
||||
{
|
||||
"name": "AND_15482_SURVEYSPARROW_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15368_VISA_PAY_REDESIGN",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15364_VISA_PAY_CARD_CLOSE",
|
||||
"version": "6.0"
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16041_VISA_TIERS_PLUS_PLAN",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "TWI_83_ADDRESS_BOOK_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "AND_15632_GASLESS_YIELD_WITHDRAW_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15235_VISA_MULTIPLE_CARDS",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15715_SWAP_BEST_DEX_RATE_ENABLED",
|
||||
"version": "6.0"
|
||||
},
|
||||
{
|
||||
"name": "AND_15767_NEW_TX_HISTORY_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -168,8 +76,40 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED",
|
||||
"version": "6.0"
|
||||
"name": "TWI_1522_MARKETING_BANNERS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1469_FOR_YOU_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1367_HIGH_FEE_WARNING_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1638_VA_MVP0_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1637_CASHBACK_AND_REACTIVATION_CAMPAIGNS_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_16080_TRON_DEX_SWAP_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1192_TANGEM_PAY_CASHBACK_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15231_STAKING_REGION_UNAVAILABLE_ENABLED",
|
||||
"version": "6.1"
|
||||
},
|
||||
{
|
||||
"name": "AND_16204_POLYMARKET_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TWI_1638_VA_MVP0_ENABLED",
|
||||
|
|
|
|||
|
|
@ -39,18 +39,7 @@ internal class FeatureTogglesNamingConventionTest {
|
|||
/** Toggles created before the AND_/TWI_ naming convention. Do NOT add new entries. */
|
||||
val EXCLUDED_TOGGLES_LIST = setOf(
|
||||
"ADDRESS_SYNC_ENABLED",
|
||||
"ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"APP_REDESIGN_ENABLED",
|
||||
"ASSETS_DISCOVERY_ENABLED",
|
||||
"DYNAMIC_ADDRESSES_ENABLED",
|
||||
"GASLESS_APPROVAL_ENABLED",
|
||||
"HEDERA_ERC20_ENABLED",
|
||||
"NEW_CARD_SCANNING_ENABLED",
|
||||
"SOLANA_SCALED_UI_AMOUNT_ENABLED",
|
||||
"SOLANA_TX_HISTORY_ENABLED",
|
||||
"STAKING_ETH_ENABLED",
|
||||
"SWAP_AB_ENABLED",
|
||||
"USEDESK_ENABLED",
|
||||
"VIRTUAL_ACCOUNTS_ENABLED",
|
||||
"VISA_ONBOARDING_ENABLED",
|
||||
"WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue