Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-23 14:34:39 +04:00
parent 8922d23eec
commit f3c2d98214
7 changed files with 962 additions and 30 deletions

View file

@ -0,0 +1,471 @@
# Navigation Graph
Complete navigation map of the app based on `AppRoute` sealed class and feature-internal routes.
## 1. All AppRoute Paths
58 top-level routes defined in `common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt`.
| # | Route | Path | Description |
|---|-------|------|-------------|
| 1 | `Initial` | `/initial` | App entry point (splash) |
| 2 | `Home` | `/home` | Stories/home screen with launch mode |
| 3 | `Welcome` | `/welcome` | Welcome screen for returning users |
| 4 | `Disclaimer` | `/disclaimer` | Terms of service / disclaimer |
| 5 | `Wallet` | `/wallet` | Main wallet portfolio screen |
| 6 | `CurrencyDetails` | `/currency_details/{walletId}/{currencyId}` | Token/coin detail screen |
| 7 | `Send` | `/send/{walletId}/{currencyId}` | Send cryptocurrency |
| 8 | `Details` | `/details/{walletId}` | Wallet details / settings hub |
| 9 | `DetailsSecurity` | `/details/security` | Security mode settings |
| 10 | `Usedesk` | `/usedesk/{walletId}` | Customer support (Usedesk) |
| 11 | `CardSettings` | `/card_settings/{walletId}` | Card-specific settings |
| 12 | `AppSettings` | `/app_settings` | Global app settings |
| 13 | `ResetToFactory` | `/reset_to_factory/{walletId}/{cardId}/...` | Factory reset flow |
| 14 | `AccessCodeRecovery` | `/access_code_recovery` | Access code recovery |
| 15 | `ManageTokens` | `{source}/manage_tokens/{accountId}` | Add/remove tokens in portfolio |
| 16 | `ChooseManagedTokens` | `/{source}/choose_managed_tokens/...` | Token chooser for send-via-swap |
| 17 | `WalletConnectSessions` | `/wallet_connect_sessions` | WalletConnect sessions list |
| 18 | `QrScanning` | `/{source}/qr_scanning` | QR code scanner |
| 19 | `ReferralProgram` | `/referral_program` | Referral program |
| 20 | `Swap` | `/swap/{fromId}/{toId}/{walletId}/...` | Token swap screen |
| 21 | `AppCurrencySelector` | `/app_currency_selector` | Fiat currency selector |
| 22 | `Staking` | `/staking/{walletId}/{currencyId}/{integrationId}` | Staking screen |
| 23 | `PushNotification` | `/push_notification` | Push notification opt-in |
| 24 | `WalletSettings` | `/wallet_settings/{walletId}` | Per-wallet settings |
| 25 | `WalletBackup` | `/wallet_backup/{walletId}/{coldOption}` | Wallet backup options |
| 26 | `WalletHardwareBackup` | `/wallet_hardware_backup/{walletId}` | Hardware wallet backup |
| 27 | `Markets` | `/markets` | Markets token list |
| 28 | `MarketsTokenDetails` | `/markets_token_details/{tokenId}/{showPortfolio}` | Market token detail |
| 29 | `Onramp` | `/onramp/{walletId}/{symbol}` | Buy crypto (onramp) |
| 30 | `OnrampSuccess` | `/onramp/success/{txId}` | Onramp success screen |
| 31 | `BuyCrypto` | `/buy_crypto/{walletId}` | Buy crypto token selector |
| 32 | `SellCrypto` | `/sell_crypto/{walletId}` | Sell crypto token selector |
| 33 | `SwapCrypto` | `/swap_crypto/{walletId}` | Swap crypto token selector |
| 34 | `Onboarding` | `/onboarding_v2/{mode}` | Onboarding flow (v2) |
| 35 | `Stories` | `/stories$storyId` | Stories / promotional content |
| 36 | `NFT` | `/nft/{walletId}` | NFT collection list |
| 37 | `NFTSend` | `/send/nft/{walletId}/{collection}/{assetId}` | Send NFT |
| 38 | `CreateWalletSelection` | `/create_wallet_selection` | Choose wallet creation type |
| 39 | `CreateWalletStart` | `/create_wallet_start` | Wallet creation intro (cold/hot) |
| 40 | `CreateHardwareWallet` | `/create_hardware_wallet` | Create hardware wallet flow |
| 41 | `CreateMobileWallet` | `/create_mobile_wallet` | Create mobile (hot) wallet |
| 42 | `UpgradeWallet` | `/upgrade_wallet/{walletId}` | Upgrade hot wallet to hardware |
| 43 | `AddExistingWallet` | `/add_existing_wallet` | Import existing wallet |
| 44 | `WalletActivation` | `/wallet_activation/{walletId}` | Activate wallet post-creation |
| 45 | `CreateWalletBackup` | `/create_wallet_backup/{walletId}` | Backup flow for created wallet |
| 46 | `UpdateAccessCode` | `/update_access_code/{walletId}` | Change access code |
| 47 | `ViewPhrase` | `/view_seed_phrase/{walletId}` | View recovery phrase |
| 48 | `ForgetWallet` | `/forget_wallet/{walletId}` | Remove wallet from app |
| 49 | `SendEntryPoint` | `/send_entry_point/{walletId}/{currencyId}` | Send entry with swap option |
| 50 | `CreateAccount` | `/create_account/{walletId}` | Create new account |
| 51 | `EditAccount` | `/edit_account/{accountId}` | Edit account |
| 52 | `AccountDetails` | `/account_details/{accountId}` | Account details screen |
| 53 | `ArchivedAccountList` | `/archived_account/{walletId}` | Archived accounts list |
| 54 | `TangemPayDetails` | `/tangem_pay_details/{walletId}` | Tangem Pay card details |
| 55 | `TangemPayOnboarding` | `/tangem_pay_onboarding/{mode}` | Tangem Pay onboarding |
| 56 | `Kyc` | `/kyc` | KYC verification |
| 57 | `YieldSupplyEntry` | `/yield_supply_entry/{walletId}/{symbol}` | Yield/supply entry point |
| 58 | `NewsDetails` | `/news_details/{newsId}` | News article detail |
## 2. Navigation Edges
Each entry shows: **Source route** → target routes it can navigate to (via `push` or `replaceAll`).
### Initial / Bootstrap
| Source | Target | Method | Trigger |
|--------|--------|--------|---------|
| `Initial` | `Home`, `Welcome`, `Disclaimer`, `Onboarding`, etc. | `replaceAll` | App startup (DefaultRoutingComponent) |
### Home
| Target | Method | Trigger |
|--------|--------|---------|
| `ManageTokens(STORIES)` | push | After scan, manage tokens |
| `CreateWalletStart` | push | Create wallet from home |
| `Wallet` | replaceAll | After wallet saved / already saved |
### Welcome
| Target | Method | Trigger |
|--------|--------|---------|
| `CreateWalletSelection` | push | "Add new wallet" button |
| `Home()` | replaceAll | When wallets list becomes empty |
| `Wallet` | replaceAll | After scan / wallet unlock / biometric |
### Disclaimer
| Target | Method | Trigger |
|--------|--------|---------|
| `PushNotification(Stories)` | push | After accepting TOS (stories flow) |
| `Home()` | replaceAll | After accepting TOS (non-stories flow) |
### Wallet (main portfolio)
| Target | Method | Trigger |
|--------|--------|---------|
| `Details` | push | Open wallet details |
| `ManageTokens(ACCOUNT)` | push | Manage tokens for account |
| `Onboarding` | push | Continue backup / onboarding |
| `CurrencyDetails` | push | Tap on a token |
| `Home` | push | Open stories |
| `NFT` | push | Open NFT collection |
| `TangemPayOnboarding` | push | Tangem Pay banner |
| `TangemPayDetails` | push | Tangem Pay card details |
| `YieldSupplyEntry` | push | Yield supply action |
| `QrScanning(MainScreen)` | push | QR scanner |
| `Send` | push | Send from QR / action |
| `WalletBackup` | push | Backup warning banner |
### CurrencyDetails (token details)
| Target | Method | Trigger |
|--------|--------|---------|
| `Onramp` | push | Buy action |
| `SendEntryPoint` | push | Send action |
| `Swap` | push | Swap action |
| `CurrencyDetails` | push | Navigate to related token (from staking router) |
| `Staking` | push | Open staking (from token details router) |
### Details (wallet details hub)
| Target | Method | Trigger |
|--------|--------|---------|
| `CreateWalletSelection` | push | Add new wallet |
| `WalletSettings` | push | Open wallet settings |
| `WalletConnectSessions` | push | WalletConnect item |
| `AppSettings` | push | App settings item |
| `Disclaimer(isTosAccepted=true)` | push | View TOS |
| `Usedesk` | push | Customer support |
| `TangemPayOnboarding(FromBannerInSettings)` | push | Tangem Pay banner |
### WalletSettings
| Target | Method | Trigger |
|--------|--------|---------|
| `ReferralProgram` | push | Referral program |
| `WalletHardwareBackup` | push | Hardware backup |
| `CardSettings` | push | Card settings |
| `ForgetWallet` | push | Delete/forget wallet |
| `ViewPhrase` | push | View seed phrase |
| `AccountDetails` | push | Open account details |
| `ArchivedAccountList` | push | View archived accounts |
| `CreateAccount` | push | Create new account |
| `Home()` | replaceAll | After wallet deletion completes |
### WalletBackup
| Target | Method | Trigger |
|--------|--------|---------|
| `WalletActivation` | push | Start activation (no backup) |
| `ViewPhrase` | push | View phrase option |
| `WalletHardwareBackup` | push | Hardware backup option |
### WalletHardwareBackup
| Target | Method | Trigger |
|--------|--------|---------|
| `CreateHardwareWallet` | push | Create new hardware wallet |
| `UpgradeWallet` | push | Upgrade current hot wallet |
| `CreateWalletBackup` | push | Backup existing wallet |
### CreateWalletSelection
| Target | Method | Trigger |
|--------|--------|---------|
| `CreateMobileWallet` | push | Choose mobile wallet |
| `CreateHardwareWallet` | push | Choose hardware wallet |
### CreateWalletStart
| Target | Method | Trigger |
|--------|--------|---------|
| `CreateMobileWallet` | push | Create mobile wallet |
| `Wallet` | replaceAll | After wallet creation completes |
### CreateMobileWallet
| Target | Method | Trigger |
|--------|--------|---------|
| `AddExistingWallet` | push | Import existing wallet |
| `Wallet` | replaceAll | After creation completes |
### CreateHardwareWallet
| Target | Method | Trigger |
|--------|--------|---------|
| `Wallet` | replaceAll | After hardware wallet created |
### AddExistingWallet
| Target | Method | Trigger |
|--------|--------|---------|
| `Wallet` | replaceAll | After import completes |
### UpgradeWallet
| Target | Method | Trigger |
|--------|--------|---------|
| `Onboarding(UpgradeHotWallet)` | push | Start upgrade onboarding |
### CreateWalletBackup
| Target | Method | Trigger |
|--------|--------|---------|
| `UpgradeWallet` | push | After backup, continue to upgrade |
### AccountDetails
| Target | Method | Trigger |
|--------|--------|---------|
| `EditAccount` | push | Edit account |
### ForgetWallet
| Target | Method | Trigger |
|--------|--------|---------|
| `Home()` | replaceAll | After wallet forgotten |
### WalletConnectSessions
| Target | Method | Trigger |
|--------|--------|---------|
| `QrScanning(WalletConnect)` | push | Scan WC QR code |
### Onboarding
| Target | Method | Trigger |
|--------|--------|---------|
| `Home()` | replaceAll | Onboarding completed (no wallets) |
| `Wallet` | replaceAll | Onboarding completed (has wallets) |
### PushNotification
| Target | Method | Trigger |
|--------|--------|---------|
| `Home()` | replaceAll | After push notification opt-in (via nextRoute param) |
### Staking
| Target | Method | Trigger |
|--------|--------|---------|
| `CurrencyDetails` | push | Back to token details |
### TangemPayDetails
| Target | Method | Trigger |
|--------|--------|---------|
| `Swap` | push | Top up / withdraw via swap |
### NFT
| Target | Method | Trigger |
|--------|--------|---------|
| `NFTSend` | push | Send NFT |
### Send (notifications)
| Target | Method | Trigger |
|--------|--------|---------|
| `CurrencyDetails` | push | Navigate to fee token |
### SwapCrypto / BuyCrypto / SellCrypto
| Target | Method | Trigger |
|--------|--------|---------|
| `Swap` | push | After token selection (SwapCrypto) |
| `Onramp` | push | After token selection (BuyCrypto/SellCrypto) |
### Deep Link Handlers (push to AppRoute)
| Handler | Target Route |
|---------|-------------|
| `OnrampDeepLinkHandler` | Processes onramp callback params |
| `SellRedirectDeepLinkHandler` | `Send` (with sell redirect params) |
| `BuyDeepLinkHandler` | `BuyCrypto` |
| `SellDeepLinkHandler` | `SellCrypto` |
| `SwapDeepLinkHandler` | `SwapCrypto` |
| `ReferralDeepLinkHandler` | Referral handling |
| `WalletDeepLinkHandler` | Wallet handling |
| `TokenDetailsDeepLinkHandler` | `CurrencyDetails` |
| `StakingDeepLinkHandler` | `Staking` |
| `MarketsDeepLinkHandler` | `Markets` |
| `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` |
| `WalletConnectDeepLinkHandler` | WalletConnect pairing |
| `PromoDeeplinkHandler` | Promo handling |
| `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` |
| `NewsDetailsDeepLinkHandler` | `NewsDetails` |
## 3. Nested Routes (Feature-Internal Navigation)
### OnboardingRoute
**File:** `features/onboarding-v2/impl/.../routing/OnboardingRoute.kt`
| Route | Description |
|-------|-------------|
| `None` | Initial empty state |
| `Note` | Single-card onboarding note |
| `MultiWallet` | Multi-wallet onboarding (with seed phrase flow option) |
| `Visa` | Visa card onboarding |
| `Twins` | Twin cards onboarding |
| `ManageTokens` | Token management during onboarding |
| `AskBiometry` | Biometry setup prompt |
| `Done` | Onboarding completion |
### WalletRoute
**File:** `features/wallet/impl/.../navigation/WalletRoute.kt`
| Route | Description |
|-------|-------------|
| `Wallet` | Main wallet view |
| `OrganizeTokens` | Reorder tokens in portfolio |
### SendEntryRoute
**File:** `features/send-v2/api/.../entry/SendEntryRoute.kt`
| Route | Description |
|-------|-------------|
| `Send` | Direct send flow |
| `SendWithSwap` | Send with swap option |
| `ChooseToken` | Token chooser for send-via-swap |
### CommonSendRoute (Send internal)
Used internally by `SendModel` and `NFTSendModel`:
- `Amount``Destination``Confirm``ConfirmSuccess`
- Edit mode: `Confirm``Destination(edit)` or `Amount(edit)`
### FeeSelectorRoute (Send internal)
- `ChooseToken` — select fee token
- `ChooseSpeed` — select fee speed
### WcInnerRoute (WalletConnect)
**File:** `features/walletconnect/impl/.../routing/WcInnerRoute.kt`
| Route | Description |
|-------|-------------|
| `Method.Send` | WC send transaction |
| `Method.SignMessage` | WC sign message |
| `Method.AddNetwork` | WC add network |
| `Method.SwitchNetwork` | WC switch network |
| `Pair` | WC pairing request |
| `UnsupportedMethodAlert` | Unsupported method alert |
| `WcDappDisconnected` | DApp disconnected alert |
| `TangemUnsupportedNetwork` | Unsupported network alert |
| `RequiredAddNetwork` | Required network add |
| `RequiredReconnectWithNetwork` | Required network reconnect |
### TangemPayDetailsInnerRoute
**File:** `features/tangempay/details/impl/.../navigation/TangemPayDetailsInnerRoute.kt`
| Route | Description |
|-------|-------------|
| `Details` | Main details view |
| `ChangePIN` | Change PIN flow |
| `ChangePINSuccess` | PIN change success |
| `AddToWallet` | Add card to device wallet |
Transitions: `Details``ChangePIN``ChangePINSuccess`, `Details``AddToWallet`
### FeedEntryRoute
**File:** `features/feed/api/.../components/FeedEntryRoute.kt`
| Route | Description |
|-------|-------------|
| `MarketTokenDetails` | Market token detail view |
| `MarketTokenList` | Markets list |
| `NewsDetail` | News article detail |
### CreateWalletBackupRoute
**File:** `features/hot-wallet/impl/.../createwalletbackup/routing/CreateWalletBackupRoute.kt`
| Route | Description |
|-------|-------------|
| `RecoveryPhraseStart` | Backup intro |
| `RecoveryPhrase` | Show recovery phrase |
| `ConfirmBackup` | Confirm backup |
| `BackupCompleted` | Backup complete (with upgrade/last screen flags) |
Transitions: `RecoveryPhraseStart``RecoveryPhrase``ConfirmBackup``BackupCompleted`
### AddExistingWalletRoute
**File:** `features/hot-wallet/impl/.../addexistingwallet/entry/routing/AddExistingWalletRoute.kt`
| Route | Description |
|-------|-------------|
| `Import` | Seed phrase import |
| `BackupCompleted` | Backup completed |
| `SetAccessCode` | Set access code |
| `ConfirmAccessCode` | Confirm access code |
| `PushNotifications` | Push notification opt-in |
| `SetupFinished` | Setup complete |
Transitions: `Import``BackupCompleted``SetAccessCode``ConfirmAccessCode``PushNotifications``SetupFinished`
### UpdateAccessCodeRoute
**File:** `features/hot-wallet/impl/.../updateaccesscode/routing/UpdateAccessCodeRoute.kt`
| Route | Description |
|-------|-------------|
| `SetAccessCode` | Enter new access code |
| `ConfirmAccessCode` | Confirm new access code |
| `SetupFinished` | Update complete |
Transitions: `SetAccessCode``ConfirmAccessCode``SetupFinished`
### WalletActivationRoute
**File:** `features/hot-wallet/impl/.../walletactivation/entry/routing/WalletActivationRoute.kt`
| Route | Description |
|-------|-------------|
| `ManualBackupStart` | Backup intro |
| `ManualBackupPhrase` | Show recovery phrase |
| `ManualBackupCheck` | Verify backup |
| `ManualBackupCompleted` | Backup success |
| `SetAccessCode` | Set access code |
| `ConfirmAccessCode` | Confirm access code |
| `PushNotifications` | Push notification opt-in |
| `SetupFinished` | Activation complete |
Transitions: `ManualBackupStart``ManualBackupPhrase``ManualBackupCheck``ManualBackupCompleted``SetAccessCode``ConfirmAccessCode``PushNotifications``SetupFinished`
## 4. Deep Links
### URI Schemes
| Scheme | Value | Usage |
|--------|-------|-------|
| `Tangem` | `tangem://` | Primary app deep links |
| `WalletConnect` | `wc://` | WalletConnect pairing |
| `Https` | `https://` | Web links (tangem.com) |
### Tangem Scheme Routes (`tangem://{host}`)
| Host | Handler | Target |
|------|---------|--------|
| `onramp` | `OnrampDeepLinkHandler` | Onramp callback processing |
| `redirect_sell` | `SellRedirectDeepLinkHandler` | `Send` (sell redirect with tx params) |
| `redirect` | — | Buy redirect (no-op) |
| `buy` | `BuyDeepLinkHandler` | `BuyCrypto` |
| `sell` | `SellDeepLinkHandler` | `SellCrypto` |
| `swap` | `SwapDeepLinkHandler` | `SwapCrypto` |
| `referral` | `ReferralDeepLinkHandler` | Referral flow |
| `main` | `WalletDeepLinkHandler` | Wallet screen |
| `token` | `TokenDetailsDeepLinkHandler` | `CurrencyDetails` |
| `staking` | `StakingDeepLinkHandler` | `Staking` |
| `markets` | `MarketsDeepLinkHandler` | `Markets` |
| `token_chart` | `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` |
| `wc` | `WalletConnectDeepLinkHandler` | WalletConnect pairing |
| `promo` | `PromoDeeplinkHandler` | Promo handling |
| `onboard-visa` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` |
### HTTPS Routes (`https://tangem.com/...`)
| Path prefix | Handler | Target |
|-------------|---------|--------|
| `/pay-app` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` |
| `/news` | `NewsDetailsDeepLinkHandler` | `NewsDetails` |
### Deep Link Readiness
Deep links are only processed when the app is on a "ready" route. These routes **block** deep link processing:
- `Initial`, `Home`, `Welcome`, `PushNotification`, `Disclaimer`, `Stories`, `Onboarding`

View file

@ -0,0 +1,215 @@
---
name: analyze-logs
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
allowed-tools: Read, Grep
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf]
---
Analyze the Tangem app user log file at path: `$ARGUMENTS`
## File Input
The user provides one or two file paths:
- **Log file** (`.txt`) — main application log, always required
- **User info file** (`.rtf` or `.txt`) — optional, contains card/device/error info from the user's feedback email
If two paths are provided, the first is the log file and the second is the user info file.
**If only the log file is provided**, ask the user if they have a user info file (`logs.rtf` or `logs.txt`). If they don't have it or don't respond, fill Device Context, Card Info, and Transaction Context sections from the log file data (Steps 2+3). Mark fields that could not be determined as "N/A".
## User Info File (logs.rtf / logs.txt)
If a user info file is provided, Read it and extract the plain text fields. The file contains structured key-value pairs like:
```
Card ID: AF36000002151580
Firmware version: 6.33r
Linked cards count: 2
Has seed phrase: true
Signed hashes [secp256k1]: 0
----------
Blockchain: Polygon
Explorer link: https://polygonscan.com/address/0x...
Derivation path: m/44'/60'/0'/0/0
Host: https://rpc-mainnet.matic.quiknode.pro/
Token: USDC
Error: Could not construct a recoverable key.
----------
Source address: 0x...
Destination address: 0x...
Amount: 11.319684
Fee: 0.004973
----------
Phone model: SM-S921B
OS version: 36
App version: 5.34.1
```
Extract all fields and include them in the **Device Context**, **Card Info**, and **Analysis Summary** sections of the report. If the RTF contains an `Error:` field, treat it as a key clue for the investigation.
Note: RTF files contain formatting markup (`\cb3`, `\cf4`, `{\field{...}}`). Ignore all RTF tags — only extract the plain text values after each colon.
## Log Format
Each line follows the pattern:
```
DD.MM HH:MM:SS.mmm: TAG Message
```
- Date format: `DD.MM` (day.month), no year — infer from context
- Multi-line entries (JSON bodies, stack traces) continue without the timestamp prefix
- Sensitive data is masked with `******`
- Continuation lines may start with `|` for structured data: `|- Duration millis: 300000`
## Analysis Steps
Use `head_limit` on every Grep call to protect context from overload.
### Step 1: Setup
1. **Log time range:** Read the first and last lines with dates (format `DD.MM`)
2. **Ask the user** (report time range, then ask):
- Date range to focus on (or "all" for the full file)
- Focus area: `Wallet`, `WalletConnect`, `Express (Onramp/Buy, Offramp/Sell, Swap/Exchange)`, `TangemPay`, `Feed`, `Markets`, `Settings`, `Referral`, `Staking`, `Onboarding`, `Send/Transactions`, `NFT`, or `all`
- Remember the chosen area as **FOCUS_AREA**
3. **Determine line range** (skip if "all"):
- Parse input into `DD.MM` patterns (`10-13.03` → start `10.03`, end `13.03`; `last day` → last date; `last 3 days` → 3 days before last)
- Find **START_LINE**: Grep `^START_DATE` (head_limit: 1, -n: true)
- Find **END_LINE**: Grep `^NEXT_DATE` (head_limit: 1, -n: true). If not found, END_LINE = end of file
- Report: "Focusing on lines START_LINEEND_LINE covering DD.MMDD.MM"
### Steps 2+3+4+5+6: Main Analysis (all in parallel)
Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (device info may be before the date range). Steps 4+5+6 use `offset: START_LINE` to stay within the date range.
**Device Context (full file):**
- `PATCH.*user-wallets/applications` (head_limit: 5, -A 10) — Read JSON body to extract `systemVersion`, `version`, `language`, `timezone`
- `ip_address` (head_limit: 5, -A 20) — extract `alpha2`, `country`, `isBuyAllowed`, `isSellAllowed`
**Card Info (full file):**
- `CardSDK_Tlv.*TAG_Firmware` (head_limit: 20)
- `CardSDK_Tlv.*TAG_SettingsMask` (head_limit: 20)
- `CardSDK_Tlv.*TAG_IsActivated` (head_limit: 20)
- `CardSDK_Tlv.*TAG_ManufacturerName` (head_limit: 20)
**Navigation (offset: START_LINE):**
- `AppRouter` (head_limit: 200) — if FOCUS_AREA is `all` or navigation-heavy (Wallet, Onboarding, Send/Transactions), also Read `.claude/docs/navigation-graph.md` to cross-reference routes
**Errors (offset: START_LINE, head_limit: 50 each, -n: true):**
- HTTP errors: `<-- [45]\d{2}`
- Domain errors: `DomainError`
- App exceptions: `\bException\b`
- Biometric errors: `onAuthenticationError`
- Tangem Pay errors: `Failed checkCustomerWallet`
**Session timeline (offset: START_LINE, head_limit: 50 each):**
- `MainActivity.*onCreate` — app session start
- `MainActivity.*Splash screen` — splash screen installed/dismissed
- `MainActivity.*onNewIntent` — deep link or push notification
- `CardSDK_Session.*start card session` — NFC session starts
**Error filtering:** When processing error results, skip these noisy matches:
- `java.io.IOException: Canceled` — normal request cancellation
- `HttpException(code=304` — HTTP "Not Modified"
- Bare stacktrace lines starting with `\tat`
- `<-- HTTP FAILED: java.io.IOException: Canceled`
### Step 7: Deep Dive
For each significant error found above:
1. Note the error's line number from Grep output (`-n: true`)
2. Use Read with `offset: ERROR_LINE - 100, limit: 200` to get ~200 lines of context
3. In that context, look for navigation events, API calls, and redux actions
## Key Tags Reference
| Tag | Purpose |
|-----|---------|
| `MainActivity` | Activity lifecycle, splash screen, onNewIntent |
| `AppRouter` | Navigation: Push, Pop, Replace |
| `NetworkLogs` | HTTP requests/responses (OkHttp) |
| `BlockchainSDK_NETWORK` | Blockchain RPC calls |
| `CardSDK_Tlv` | NFC card data (firmware, settings) |
| `CardSDK_Session` | NFC session lifecycle |
| `CardSDK_Biometric` | Biometric authentication |
## Common Error Patterns
| Pattern | Meaning |
|---------|---------|
| `HttpException(code=4xx/5xx, errorBody={...})` | API error with structured body |
| `DomainError(description=...)` | App-level domain error |
| `<-- HTTP FAILED: java.io.IOException: Canceled` | Cancelled network request (noise) |
| `<-- 429` | Rate limiting |
| `onAuthenticationError` | Biometric auth failure |
## Output Template
Structure your report EXACTLY as follows:
```
# Log Analysis Report
## Device Context
| Parameter | Value |
|-----------|-------|
| App version | ... |
| Android version | ... |
| Phone model | ... (from logs.rtf if available) |
| Language | ... |
| Timezone | ... |
| Country | ... |
| Log time range | DD.MM HH:MM — DD.MM HH:MM |
## Card Info
| Parameter | Value |
|-----------|-------|
| Card ID | ... (from logs.rtf if available) |
| Firmware | ... |
| Manufacturer | ... |
| Is activated | ... |
| Linked cards | ... (from logs.rtf if available) |
| Has seed phrase | ... (from logs.rtf if available) |
## Transaction Context (from logs.rtf, if available)
| Parameter | Value |
|-----------|-------|
| Blockchain | ... |
| Token | ... |
| Source address | ... |
| Destination address | ... |
| Amount | ... |
| Fee | ... |
| Error | ... |
## Navigation Path
1. [HH:MM:SS] Screen (Push/Pop/Replace)
2. ...
**Summary:** Brief description of the user's journey.
## Errors Found
### HTTP Errors
| Time | URL | Status | Details |
|------|-----|--------|---------|
### Domain Errors
| Time | Component | Error |
|------|-----------|-------|
### Other Errors
| Time | Type | Details |
|------|------|---------|
## Key Events Timeline
| Time | Event | Details |
|------|-------|---------|
(chronological: app starts, card sessions, navigation, errors, notable API calls)
## Analysis Summary
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)
```
If a section has no data, write "None found" instead of omitting it.

View file

@ -174,6 +174,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
installAppTheme()
val splashScreen = installSplashScreen()
TangemLogger.i("Splash screen installed")
enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(
@ -350,6 +351,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
if (isFromPush) {

View file

@ -270,22 +270,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
}
}
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
if (!BuildConfig.TESTER_MENU_ENABLED) {
appLogsStore.deleteLastLogFile()
}
// Temporally logs are not saved
// scope.launch {
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
// appLogsStore.deleteLastLogFile()
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
// }
// }
}
/**
* Initialize components that need to be initialized before [super.onCreate] is called
*/
@ -389,6 +373,22 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
)
}
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
if (!BuildConfig.TESTER_MENU_ENABLED) {
appLogsStore.deleteLastLogFile()
}
// Temporarily logs are not saved
// scope.launch {
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
// appLogsStore.deleteLastLogFile()
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
// }
// }
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(
context = this,

View file

@ -149,6 +149,7 @@ internal class MainViewModel @Inject constructor(
// await while initial route stack is initialized
appRouterConfig.initializedState.first { it }
TangemLogger.withTag("MainActivity").i("Splash screen dismissed")
isSplashScreenShown = false
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.navigation.Router
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.coroutines.CoroutineScope
@ -19,6 +20,8 @@ internal class ProxyAppRouter(
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : AppRouter {
private val logger = TangemLogger.withTag("AppRouter")
private val routerScope: CoroutineScope
get() = requireNotNull(config.routerScope) {
"Router scope is not set in config"
@ -47,12 +50,8 @@ internal class ProxyAppRouter(
}
override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
safeNavigate(onComplete, message = "Replace all routes with $routes") {
runCatching {
innerRouter.replaceAll(*routes, onComplete = onComplete)
}.getOrElse {
TangemLogger.e("Error", it)
}
safeNavigate(onComplete, message = "Replace all routes with ${routes.toList().ifEmpty { "<empty>" }}") {
innerRouter.replaceAll(*routes, onComplete = onComplete)
}
}
@ -76,21 +75,20 @@ internal class ProxyAppRouter(
private fun safeNavigate(onComplete: (isSuccess: Boolean) -> Unit, message: String, block: () -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
TangemLogger.i(message)
logger.i(message)
try {
block()
} catch (e: Throwable) {
TangemLogger.e("Error", e)
onComplete(false)
}
runSuspendCatching(block = { block() })
.onFailure { throwable ->
logger.e(messageString = "Error", throwable = throwable)
onComplete(false)
}
}
}
override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) {
if (!isSuccess) {
analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage)))
TangemLogger.w(errorMessage)
logger.w(errorMessage)
with(receiver = config.snackbarHandler ?: return) {
showSnackbar(

View file

@ -0,0 +1,245 @@
package com.tangem.tap.routing
import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.navigation.Router
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@OptIn(ExperimentalCoroutinesApi::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class ProxyAppRouterTest {
private val innerRouter = mockk<Router>(relaxed = true)
private val snackbarHandler = mockk<SnackbarHandler>(relaxed = true)
private val analyticsExceptionHandler = mockk<AnalyticsExceptionHandler>(relaxed = true)
private val dispatchers = TestingCoroutineDispatcherProvider()
private val config = mockk<AppRouterConfig>(relaxed = true) {
every { componentRouter } returns innerRouter
every { stack } returns listOf(AppRoute.Wallet)
every { snackbarHandler } returns this@ProxyAppRouterTest.snackbarHandler
every { initializedState } returns MutableStateFlow(true)
}
@AfterEach
fun tearDown() {
clearMocks(innerRouter, snackbarHandler, analyticsExceptionHandler, config)
every { config.componentRouter } returns innerRouter
every { config.stack } returns listOf(AppRoute.Wallet)
every { config.snackbarHandler } returns snackbarHandler
every { config.initializedState } returns MutableStateFlow(true)
}
private fun createRouter(routerScope: CoroutineScope): ProxyAppRouter {
every { config.routerScope } returns routerScope
return ProxyAppRouter(config, dispatchers, analyticsExceptionHandler)
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Push {
@Test
fun `delegates to inner router`() = runTest {
// Arrange
val router = createRouter(this)
val route = AppRoute.AppSettings
// Act
router.push(route)
// Assert
verify { innerRouter.push(route, any()) }
}
@Test
fun `calls onComplete false when inner router throws`() = runTest {
// Arrange
val router = createRouter(this)
every { innerRouter.push(any(), any()) } throws RuntimeException("Navigation error")
var result: Boolean? = null
// Act
router.push(AppRoute.AppSettings) { result = it }
// Assert
assertThat(result).isFalse()
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class ReplaceCurrent {
@Test
fun `delegates to inner router`() = runTest {
// Arrange
val router = createRouter(this)
val route = AppRoute.AppSettings
// Act
router.replaceCurrent(route)
// Assert
verify { innerRouter.replaceCurrent(route, any()) }
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class ReplaceAll {
@Test
fun `delegates to inner router`() = runTest {
// Arrange
val router = createRouter(this)
val route = AppRoute.Wallet
// Act
router.replaceAll(route)
// Assert
verify { innerRouter.replaceAll(route, onComplete = any()) }
}
@Test
fun `catches exception silently via safeNavigate`() = runTest {
// Arrange
val router = createRouter(this)
every { innerRouter.replaceAll(*anyVararg(), onComplete = any()) } throws RuntimeException("Error")
// Act
val actual = runCatching { router.replaceAll(AppRoute.Wallet) }.isSuccess
// Assert
assertThat(actual).isTrue()
verify { innerRouter.replaceAll(AppRoute.Wallet, onComplete = any()) }
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Pop {
@Test
fun `delegates to inner router`() = runTest {
// Arrange
val router = createRouter(this)
// Act
router.pop()
// Assert
verify { innerRouter.pop(any()) }
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class PopTo {
@Test
fun `delegates to inner router with route`() = runTest {
// Arrange
val router = createRouter(this)
val route = AppRoute.Wallet
// Act
router.popTo(route)
// Assert
verify { innerRouter.popTo(route, any()) }
}
@Test
fun `delegates to inner router with routeClass`() = runTest {
// Arrange
val router = createRouter(this)
// Act
router.popTo(AppRoute.Wallet::class)
// Assert
verify { innerRouter.popTo(AppRoute.Wallet::class, any()) }
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class DefaultCompletionHandler {
@Test
fun `does nothing on success`() {
// Arrange
val router = createRouter(mockk())
// Act
router.defaultCompletionHandler(isSuccess = true, errorMessage = "error")
// Assert
verify(exactly = 0) { analyticsExceptionHandler.sendException(any()) }
verify(exactly = 0) { snackbarHandler.showSnackbar(text = any<Int>(), buttonTitle = any(), action = any()) }
}
@Test
fun `sends analytics and shows snackbar on failure`() {
// Arrange
val router = createRouter(mockk())
// Act
router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed")
// Assert
verify { analyticsExceptionHandler.sendException(any<ExceptionAnalyticsEvent>()) }
verify { snackbarHandler.showSnackbar(text = any<Int>(), buttonTitle = any(), action = any()) }
}
@Test
fun `sends analytics without snackbar when handler is null`() {
// Arrange
every { config.snackbarHandler } returns null
val router = createRouter(mockk())
// Act
router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed")
// Assert
verify { analyticsExceptionHandler.sendException(any<ExceptionAnalyticsEvent>()) }
verify(exactly = 0) { snackbarHandler.showSnackbar(text = any<Int>(), buttonTitle = any(), action = any()) }
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Stack {
@Test
fun `returns config stack`() {
// Arrange
val router = createRouter(mockk())
val expected = listOf(AppRoute.Wallet)
// Act
val actual = router.stack
// Assert
assertThat(actual).isEqualTo(expected)
}
}
}