From b63f8be2ce051ad8a916be8b41503d5788a9b160 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Jul 2026 14:44:02 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .claude/docs/running-the-app.md | 74 ++++++++ .mcp.json | 7 +- build.gradle.kts | 289 ++++++++++++++++++++++++++++++++ common/build.gradle.kts | 35 ++-- gradle/dependencies.toml | 1 - 5 files changed, 384 insertions(+), 22 deletions(-) create mode 100644 .claude/docs/running-the-app.md diff --git a/.claude/docs/running-the-app.md b/.claude/docs/running-the-app.md new file mode 100644 index 0000000000..0b03ca64d0 --- /dev/null +++ b/.claude/docs/running-the-app.md @@ -0,0 +1,74 @@ +# Running the App on a Device (ADB) + +Practical notes for launching the app on a connected device for manual/on-device testing. + +## Package & main activity + +| | Value | +|---|---| +| Debug package (Google flavor) | `com.tangem.wallet.debug` | +| Main launcher activity | `com.tangem.tap.MainActivity` | +| Application class | `com.tangem.tap.TangemHiltApplication` | + +## Launch the main screen (correct way) + +```bash +adb shell am force-stop com.tangem.wallet.debug +adb shell am start -n com.tangem.wallet.debug/com.tangem.tap.MainActivity +``` + +Verify the real screen is in the foreground: + +```bash +adb shell dumpsys activity activities | grep -i topResumedActivity | grep -i tangem +# expect: topResumedActivity=ActivityRecord{... com.tangem.wallet.debug/com.tangem.tap.MainActivity ...} +``` + +## Pitfall: do NOT launch via `monkey` + +```bash +# ❌ ambiguous — may open the wrong screen +adb shell monkey -p com.tangem.wallet.debug -c android.intent.category.LAUNCHER 1 +``` + +Debug builds bundle **LeakCanary**, which registers its own `LAUNCHER` activity ("Leaks"). +The package therefore has **multiple** MAIN/LAUNCHER activities, so `monkey` (and +`cmd package resolve-activity`) resolves to Android's `ResolverActivity` / the wrong entry +and can open **LeakCanary instead of the app**. Always launch `MainActivity` explicitly with +`am start -n`. + +List the launcher activities to confirm: + +```bash +adb shell cmd package query-activities \ + -a android.intent.action.MAIN -c android.intent.category.LAUNCHER \ + | grep -iE "name=" | grep -i tangem +``` + +## Cold start vs warm start (matters for `Application.init()`) + +`TangemApplication.init()` (Hilt setup, and one-time bootstrap such as backend-auth device +key generation + registration) runs **once per process**, on `Application.onCreate()`. + +- **Warm start** (process already alive / resumed from background): `init()` does **not** re-run. + You will NOT see the startup log banner `APP STARTED` or any one-time bootstrap logs. +- **Cold start** (after `am force-stop`, or first launch): `init()` runs → look for the + `APP STARTED` banner in logcat (tag `TangemApplication`). + +To reliably observe anything that happens in `init()`, always `force-stop` first, then launch. + +> Note: `init()` runs on process start regardless of *which* activity brought the process up +> (even LeakCanary's). So process-scoped logs are still valid on a wrong-activity launch — but +> for observing actual app UI/flows, launch `MainActivity` explicitly. + +## Build & install (Google debug) + +```bash +./gradlew :app:assembleGoogleDebug +adb install -r app/build/outputs/apk/google/debug/app-google-debug.apk +``` + +## Product flavors + +Flavors: `google`, `huawei` (dimension `services`); default dev flavor is `google`. +Build types: `debug`, `mocked`, `internal`, `external`, `release`. \ No newline at end of file diff --git a/.mcp.json b/.mcp.json index 3721a9071a..ae41858367 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,15 +5,10 @@ "command": "npx", "args": ["-y", "firebase-tools@latest", "mcp"] }, - "atlassian": { - "type": "stdio", - "command": "npx", - "args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"] - }, "notion": { "type": "stdio", "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.notion.com/mcp"] } } -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index b61f203435..bfb7bf01f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,8 @@ plugins { alias(deps.plugins.room) apply false alias(deps.plugins.kotlin.compose.compiler) apply false alias(deps.plugins.ksp) apply false + // Applied (not `apply false`) so the root project gets the aggregate `buildHealth` task. + alias(deps.plugins.dependency.analysis) } buildscript { @@ -21,6 +23,288 @@ buildscript { } } +// Dependency Analysis (DAGP) global configuration. +dependencyAnalysis { + structure { + // Treat co-versioned artifact splits as a single logical dependency, so the plugin doesn't + // advise "declare the transitive directly" when you depend on one part and use a sibling. + // e.g. `serialization-json` always brings `serialization-core` (@Serializable lives there). + bundle("kotlinx-serialization") { + includeDependency("org.jetbrains.kotlinx:kotlinx-serialization-core") + includeDependency("org.jetbrains.kotlinx:kotlinx-serialization-json") + } + // Hilt/Dagger is split across many artifacts (hilt-android pulls dagger, hilt-core, + // javax.inject; hilt-compiler pulls dagger-compiler). Declaring hilt-android + hilt-compiler + // is the single entry point — don't advise expanding each transitive separately. + bundle("hilt") { + primary("com.google.dagger:hilt-android") + includeGroup("com.google.dagger") + includeDependency("javax.inject:javax.inject") + } + // `junit-jupiter` is an aggregator over `-api`/`-params`/`-engine`. Tests import the `-api` + // package; declaring the `junit-jupiter` aggregator (deps.test.junit5) is the project standard. + bundle("junit5") { + primary("org.junit.jupiter:junit-jupiter") + includeGroup("org.junit.jupiter") + } + // MockK's DSL functions (every/coEvery/verify) live in `mockk-dsl`, pulled by `mockk`. + // Declaring `mockk` (deps.test.mockk) is enough. + bundle("mockk") { + primary("io.mockk:mockk") + includeGroup("io.mockk") + } + // DataStore is split into datastore-core / datastore-preferences-core / datastore-preferences, + // all co-versioned. Declaring the one you use covers the sibling packages. + bundle("androidx-datastore") { + includeGroup("androidx.datastore") + } + // web3j is split across core/abi/crypto/tuples (+ its rxjava reactive layer), all co-versioned. + // The generated Solidity contract wrappers expose these in their public API; declaring + // web3j-core (the entry point) covers the family. rxjava is web3j-only in this repo. + bundle("web3j") { + primary("org.web3j:core") + includeGroup("org.web3j") + includeDependency("io.reactivex.rxjava2:rxjava") + } + // Decompose's public API (ComponentContext etc.) extends Essenty owner interfaces, so the + // essenty artifacts are part of decompose's surface. Declaring `decompose` covers them. + bundle("decompose") { + primary("com.arkivanov.decompose:decompose") + includeGroup("com.arkivanov.decompose") + includeGroup("com.arkivanov.essenty") + } + // Jetpack Compose is split across ~15 co-versioned artifacts (ui, ui-graphics, ui-text, + // ui-unit, foundation, foundation-layout, animation, animation-core, runtime, runtime-saveable, + // material-ripple, …). Treat the whole `androidx.compose.*` family as one logical dependency. + bundle("compose") { + include("^androidx\\.compose\\..*") + } + // Co-versioned families split across many artifacts — declaring one covers the siblings. + bundle("androidx-lifecycle") { + primary("androidx.lifecycle:lifecycle-runtime-ktx") + includeGroup("androidx.lifecycle") + } + bundle("androidx-appcompat") { + primary("androidx.appcompat:appcompat") + includeGroup("androidx.appcompat") + } + bundle("androidx-paging") { + includeGroup("androidx.paging") + } + bundle("coil") { + includeGroup("io.coil-kt") + } + // Room is split into runtime/ktx/common/compiler/paging (+ its androidx.sqlite runtime). + // Declaring room-runtime covers the siblings. + bundle("room") { + primary("androidx.room:room-runtime") + includeGroup("androidx.room") + includeGroup("androidx.sqlite") + } + } + issues { + all { + // hilt-android / hilt-core get flagged as `api`: Kotlin `internal` @Inject classes (and + // public @Inject impls) compile to public bytecode carrying RUNTIME javax.inject annotations, + // so DAGP sees javax.inject in the ABI. The Hilt runtime is never genuinely public API — keep + // it `implementation` repo-wide. + onIncorrectConfiguration { + exclude( + "com.google.dagger:hilt-android", + "com.google.dagger:hilt-core", + ) + } + // dagger-compiler is always pulled by hilt-compiler (declared via kapt(deps.hilt.kapt)); + // no need to declare the annotation processor separately. + // :test:core is the documented single entry point for the unit-test stack — it re-exports + // junit5/mockk/truth/turbine/coroutines-test as `api`. Because those re-exports are excluded + // from the "declare directly" advice above, DAGP sees :test:core itself as unused. It isn't — + // keep declaring testImplementation(projects.test.core) and silence the false positive. + onUnusedDependencies { + exclude(":test:core") + } + onUsedTransitiveDependencies { + exclude("com.google.dagger:dagger-compiler") + // jsr305 is a ubiquitous CLASS-retention nullability-annotation transitive (pulled in + // by many libs); consumers tolerate its absence, it's never declared directly. + exclude("com.google.code.findbugs:jsr305") + // :test:core re-exports the unit-test stack as `api` and is the documented single + // entry point (testImplementation(projects.test.core)). Don't advise declaring its + // re-exports directly in every test module. + exclude( + "app.cash.turbine:turbine", + "com.google.truth:truth", + "io.mockk:mockk", + "org.jetbrains.kotlinx:kotlinx-coroutines-test", + "org.junit.jupiter:junit-jupiter", + ) + // wire-runtime (protobuf) and jakarta.inject-api are deep transitives of the Visa / + // WalletConnect (reown) SDKs. A few generated/SDK-facing types reference them, but they're + // never declared directly — they always arrive with their owning SDK. Don't advise + // declaring them per-module. + exclude( + "com.squareup.wire:wire-runtime", + "jakarta.inject:jakarta.inject-api", + ) + } + } + // In :libs:auth these are injected only into `internal` classes (DI modules / Default* impls). + // DAGP advises `api` because Kotlin `internal` compiles to public bytecode (and Hilt's + // generated `_Factory` classes expose the constructor types publicly) — a false positive, not + // a real ABI leak. Keep them `implementation`. Scoped to this module so genuine api advice in + // other modules still surfaces. + project(":libs:auth") { + onIncorrectConfiguration { + exclude( + "com.squareup.moshi:moshi", + ":core:config-toggles", + ":core:datasource", + ":core:utils", + ) + } + } + // Same Kotlin-`internal`-compiles-to-public false positive: these are used only inside + // `internal` classes / DI modules / method bodies (verified), not in the public ABI. + project(":libs:blockchain-sdk") { + onIncorrectConfiguration { + exclude( + "androidx.datastore:datastore-preferences", + "com.squareup.moshi:moshi", + ":core:analytics", + ":core:utils", + ) + } + } + // config-toggles is used only inside the `internal` DefaultCardSdkFeatureToggles (the public + // CardSdkFeatureToggles interface is empty) — internal→public false positive. + project(":libs:tangem-sdk-api") { + onIncorrectConfiguration { + exclude(":core:config-toggles") + } + } + // web3j leaks into the ABI only via incidentally-public generated contract wrappers + // (ERC20/TangemPaymentAccount/…), not the module's intended public API (VisaContractInfoProvider + // / VisaContractInfo) — keep it `implementation` (see PR review), so silence the `api` advice. + project(":libs:visa") { + onIncorrectConfiguration { + exclude("org.web3j:core") + } + } + // datasource/utils are used only inside the `internal` DI module + Amplitude impl (the public + // ABTestsManager interface doesn't expose them) — internal→public false positive. + project(":core:ab-tests") { + onIncorrectConfiguration { + exclude(":core:datasource", ":core:utils") + } + } + // datasource is used only inside `internal` DI modules + LocalTogglesStorage (the public + // FeatureTogglesManager API doesn't expose it) — internal→public false positive. + project(":core:config-toggles") { + onIncorrectConfiguration { + exclude(":core:datasource") + } + } + // moshi-polymorphic-adapter is used only in @Provides bodies / as annotation args, never in a + // public signature — internal→public false positive, keep it `implementation`. + project(":core:datasource") { + onIncorrectConfiguration { + exclude("dev.onenowy.moshipolymorphicadapter:moshi-polymorphic-adapter") + } + } + // material is consumed only via resources (styles.xml inherits MaterialComponents themes), which + // DAGP can't see — it suggests runtimeOnly, but resource linking needs it on the compile + // classpath. Keep it `implementation`. + project(":core:ui") { + onRuntimeOnly { + exclude("com.google.android.material:material") + } + } + // core:error exposes UniversalError, a supertype of VisaActivationError that card consumes via + // domain:visa:models. The compiler needs the supertype on the classpath, but DAGP sees no direct + // reference and flags it unused — false positive, keep it `implementation`. + project(":domain:card") { + onUnusedDependencies { + exclude(":core:error") + } + } + // core:utils is deliberately re-exported as api from the ubiquitous domain:models module so the + // many consumers that use TangemLogger / utils through it keep compiling. Demoting it to + // implementation would cascade across the whole repo, so silence the incorrect-config advice. + project(":domain:models") { + onIncorrectConfiguration { + exclude(":core:utils") + } + } + // :domain:models is kept as api because domain:markets:models exposes CryptoCurrency.RawID (a + // domain:models type) in its public data classes (TokenMarket/RawMarketToken/TokenMarketParams). + // DAGP misses this nested-type ABI leak and advises implementation; that advice is a false negative. + project(":domain:markets:models") { + onIncorrectConfiguration { + exclude(":domain:models") + } + } + // :domain:core is kept as api in :domain:legacy and :domain:express because their public APIs + // (RampStateManager, ExpressServiceFetcher#getInitializationStatus) return Flow> where Lce + // is a domain:core type. DAGP doesn't trace the generic type argument into the ABI and advises + // implementation; that advice is a false negative. + project(":domain:legacy") { + onIncorrectConfiguration { + exclude(":domain:core") + } + } + project(":domain:express") { + onIncorrectConfiguration { + exclude(":domain:core") + } + } + // :domain:core kept as api in :domain:onramp — public use cases (GetOnrampCurrenciesUseCase etc.) + // return EitherFlow<...> (a domain:core alias). DAGP doesn't trace the alias/generic into the ABI. + project(":domain:onramp") { + onIncorrectConfiguration { + exclude(":domain:core") + } + } + // arrow-core kept as api in :domain:onboarding — WasTwinsOnboardingShownUseCase#invoke returns + // Flow> (arrow.core.Either in the public ABI). DAGP advises implementation here (false + // negative on the generic type argument). + project(":domain:onboarding") { + onIncorrectConfiguration { + exclude("io.arrow-kt:arrow-core") + } + } + // :domain:core kept as api in :domain:wallets — GetUserWalletUseCase#invokeFlow returns + // EitherFlow<...> (a domain:core alias). DAGP false-negative on the alias/generic. + project(":domain:wallets") { + onIncorrectConfiguration { + exclude(":domain:core") + } + } + // :domain:models kept as api in :domain:yield-supply:models — YieldMarketToken (public data + // class) exposes SerializedBigDecimal (a domain:models type) in public fields. DAGP misses it. + project(":domain:yield-supply:models") { + onIncorrectConfiguration { + exclude(":domain:models") + } + } + // :common is deliberately re-exported as api from common:ui (a ubiquitous UI dependency) so the + // many feature modules that use TangemBlogUrlBuilder / common types through it keep compiling. + // Demoting it to implementation would cascade across the feature graph. + project(":common:ui") { + onIncorrectConfiguration { + exclude(":common") + } + } + // libs:crypto is deliberately re-exported as api from the ubiquitous :common module so the many + // feature modules that use BlockchainUtils / crypto helpers through it keep compiling. Demoting + // it to implementation would cascade across the feature graph. + project(":common") { + onIncorrectConfiguration { + exclude(":libs:crypto") + } + } + } +} + val clean by tasks.registering { delete(rootProject.buildDir) } @@ -37,6 +321,11 @@ val unitTest by tasks.registering { } subprojects { + // Dependency Analysis (DAGP) registers `projectHealth`/`reason` on each module. In 3.x the + // root application no longer auto-applies to subprojects, so apply it here. Reusing the plugin + // already resolved by the root `plugins {}` block keeps it in the same classloader as AGP/Kotlin. + apply(plugin = "com.autonomousapps.dependency-analysis") + // App module plugins.withId("com.android.application") { afterEvaluate { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 75f81bff56..d09352ea5a 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,8 +1,6 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.hilt.android) id("configuration") } @@ -12,28 +10,35 @@ android { dependencies { - implementation(projects.core.utils) - api(projects.domain.models) - api(projects.domain.appCurrency.models) - api(projects.domain.staking.models) - api(projects.libs.crypto) + // region Other libraries + implementation(deps.hilt.android) + // endregion // region Firebase libraries implementation(platform(deps.firebase.bom)) implementation(deps.firebase.analytics) implementation(deps.firebase.crashlytics) implementation(deps.firebase.messaging) - // end + // endregion + // region Core modules + implementation(projects.core.utils) + // endregion - implementation(deps.arrow.core) + // region Domain + api(projects.domain.models) + // endregion - testImplementation(projects.test.core) + // region Libs + // libs:crypto is intentionally re-exported (api): :common is ubiquitous and many feature modules + // use BlockchainUtils / crypto helpers through it. Demoting to implementation cascades across the + // feature graph, so keep it api despite DAGP's advice (suppressed below). + api(projects.libs.crypto) + // endregion + + // region Tests testImplementation(deps.test.junit5) testImplementation(deps.test.truth) - - // region DI - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - // end + testImplementation(projects.test.core) + // endregion } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4d873ab33d..cd1108a955 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -264,7 +264,6 @@ coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } kotlin-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutine" } -kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" } kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" } kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" } desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" }