From f3c2d98214442efb89d20fa2d3a509216fe83d0e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Mar 2026 14:34:39 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .claude/docs/navigation-graph.md | 471 ++++++++++++++++++ .claude/skills/analyze-logs/SKILL.md | 215 ++++++++ .../main/java/com/tangem/tap/MainActivity.kt | 2 + .../java/com/tangem/tap/TangemApplication.kt | 32 +- .../tangem/tap/features/main/MainViewModel.kt | 1 + .../com/tangem/tap/routing/ProxyAppRouter.kt | 26 +- .../tangem/tap/routing/ProxyAppRouterTest.kt | 245 +++++++++ 7 files changed, 962 insertions(+), 30 deletions(-) create mode 100644 .claude/docs/navigation-graph.md create mode 100644 .claude/skills/analyze-logs/SKILL.md create mode 100644 app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt diff --git a/.claude/docs/navigation-graph.md b/.claude/docs/navigation-graph.md new file mode 100644 index 0000000000..9e5202e1ca --- /dev/null +++ b/.claude/docs/navigation-graph.md @@ -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` \ No newline at end of file diff --git a/.claude/skills/analyze-logs/SKILL.md b/.claude/skills/analyze-logs/SKILL.md new file mode 100644 index 0000000000..4ecc6e0104 --- /dev/null +++ b/.claude/skills/analyze-logs/SKILL.md @@ -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_LINE–END_LINE covering DD.MM–DD.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. \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index dd856fb233..f6a2ff3217 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 835298b890..01efaeb7bf 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index be57cd0aa2..7b156a7889 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index d9cd7759f2..a78fc262d8 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -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 { "" }}") { + 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( diff --git a/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt new file mode 100644 index 0000000000..264a9b3f0a --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt @@ -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(relaxed = true) + private val snackbarHandler = mockk(relaxed = true) + private val analyticsExceptionHandler = mockk(relaxed = true) + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val config = mockk(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(), 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()) } + verify { snackbarHandler.showSnackbar(text = any(), 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()) } + verify(exactly = 0) { snackbarHandler.showSnackbar(text = any(), 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) + } + } +} \ No newline at end of file