Updated on 2026-08-14
This commit is contained in:
parent
8922d23eec
commit
f3c2d98214
7 changed files with 962 additions and 30 deletions
471
.claude/docs/navigation-graph.md
Normal file
471
.claude/docs/navigation-graph.md
Normal 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`
|
||||
215
.claude/skills/analyze-logs/SKILL.md
Normal file
215
.claude/skills/analyze-logs/SKILL.md
Normal 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_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.
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
245
app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt
Normal file
245
app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue