Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-25 13:47:05 +03:00
commit 467442724d
595 changed files with 7210 additions and 3334 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,19 @@
# Image Resources
## Naming
There are 3 types of icons:
1. Black or single color icon (naming: `ic_name_24`, where number is size)
2. Icon with constant color, and tint could be applied (naming: `img_name_24`)
3. Large image with different colors and shapes (naming: `ill_name`)
Examples:
1. `ic_chevron_24`
2. `img_walletconnect_24`
3. `ill_bussiness`
## Attention
For complex vector images (named with `ill_name`), you should use `.png` resources, because when the project is compiled, all complex vectors are converted to large, heavy PNGs for different dimensions.

View file

@ -0,0 +1,107 @@
# Domain Components
Key domain mechanisms that orchestrate data flow: suppliers, fetchers, and use cases.
## Retrieving Core Models
### UserWallet
#### UserWalletsListRepository
**Location:** `domain/common``com.tangem.domain.common.wallets.UserWalletsListRepository`
Repository for managing user wallets list. Provides `StateFlow<List<UserWallet>?>` for the wallets list and `StateFlow<UserWallet?>` for the selected wallet. Supports loading, selecting, saving, locking/unlocking (biometric, access code), deleting, and reordering wallets.
### Account / AccountList
#### SingleAccountSupplier
**Location:** `domain/account``com.tangem.domain.account.supplier.SingleAccountSupplier`
Supplier that provides a single `Account` by `AccountId`. Has convenience methods `filterPaymentAccount` and `filterCryptoPortfolioAccount` to filter by account subtype.
#### SingleAccountListSupplier
**Location:** `domain/account``com.tangem.domain.account.supplier.SingleAccountListSupplier`
Supplier that provides an `AccountList` for a specific user wallet by `UserWalletId`.
#### MultiAccountListSupplier
**Location:** `domain/account``com.tangem.domain.account.supplier.MultiAccountListSupplier`
Supplier that provides a list of `AccountList`s for all user wallets. Extends `FlowCachingSupplier`.
#### SingleAccountListFetcher
**Location:** `domain/account``com.tangem.domain.account.fetcher.SingleAccountListFetcher`
Fetcher that fetches a list of accounts for a single wallet by `UserWalletId`. Extends `FlowFetcher`.
### AccountStatus / AccountStatusList
#### SingleAccountStatusSupplier
**Location:** `domain/account/status``com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier`
Supplier that provides a single `AccountStatus` by account identifier. Extends `FlowCachingSupplier`.
#### SingleAccountStatusListSupplier
**Location:** `domain/account/status``com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier`
Same as `SingleAccountListSupplier` but provides `AccountStatusList` (accounts with balances) for a specific user wallet.
#### MultiAccountStatusListSupplier
**Location:** `domain/account/status``com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier`
Same as `MultiAccountListSupplier` but provides a list of `AccountStatusList`s for all user wallets.
### Network / NetworkStatus
#### SingleNetworkStatusSupplier
**Location:** `domain/networks``com.tangem.domain.networks.single.SingleNetworkStatusSupplier`
Supplier of `NetworkStatus` for a specific network and wallet. Extends `FlowCachingSupplier`.
#### MultiNetworkStatusSupplier
**Location:** `domain/networks``com.tangem.domain.networks.multi.MultiNetworkStatusSupplier`
Supplier of all `NetworkStatus`es (as `Set<NetworkStatus>`) for a selected wallet. Extends `FlowCachingSupplier`.
#### SingleNetworkStatusFetcher
**Location:** `domain/networks``com.tangem.domain.networks.single.SingleNetworkStatusFetcher`
Fetcher of network status for a single `Network` by `UserWalletId`. Extends `FlowFetcher`.
#### MultiNetworkStatusFetcher
**Location:** `domain/networks``com.tangem.domain.networks.multi.MultiNetworkStatusFetcher`
Fetcher of network statuses for a set of `Network`s for a multi-currency wallet by `UserWalletId`. Extends `FlowFetcher`.
## Updating Balances
### WalletBalanceFetcher
**Location:** `domain/tokens``com.tangem.domain.tokens.wallet.WalletBalanceFetcher`
Fetcher of wallet balances by `UserWalletId`. Selects the appropriate fetching strategy based on wallet type (multi-wallet, single wallet with tokens, single wallet). Delegates to `BalanceFetchingOperations` for shared fetching logic.
### CryptoCurrencyBalanceFetcher
**Location:** `domain/account/status``com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher`
Fetches and refreshes balances for specific crypto currencies. Uses per-wallet mutexes to allow concurrent refreshes for different wallets while preventing concurrent refreshes for the same wallet. Delegates to `BalanceFetchingOperations`.
## Managing Portfolio (User Tokens)
### ManageCryptoCurrenciesUseCase
**Location:** `domain/account/status``com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase`
Use case for adding and removing crypto currencies in an account.

View file

@ -0,0 +1,142 @@
# Domain Models
Core business models used across the application. Models are defined in `domain/models/` and `domain/account/`.
## StatusSource
**Location:** `domain/models``com.tangem.domain.models.StatusSource`
Enum representing data loading/refresh status. Used across all status models (NetworkStatus, QuoteStatus, YieldBalance, CryptoCurrencyStatus.Sources):
- `CACHE` — initial status, data loaded from cache
- `ACTUAL` — terminal status, data successfully fetched from server
- `ONLY_CACHE` — terminal status, data could not be refreshed (only cached data available)
## CryptoCurrency
**Location:** `domain/models``com.tangem.domain.models.currency.CryptoCurrency`
Sealed class representing a cryptocurrency — either a native coin (`Coin`) or a token (`Token`). Used throughout the application: portfolio, token search, swaps, buy/sell, staking, etc.
## CryptoCurrencyStatus
**Location:** `domain/models``com.tangem.domain.models.currency.CryptoCurrencyStatus`
Model representing a currency with its balance state. Primarily used to display user's coin balance in the portfolio. Wraps `CryptoCurrency` with a `Value` sealed interface:
| Value subtype | Description |
|---|---|
| `Loading` | First-time fetch; once data is loaded, subsequent updates use cache via StatusSource, bypassing Loading |
| `Loaded` | Full data available |
| `Custom` | Custom token in portfolio; some data may be missing (e.g., no balance if backend has no quotes for it) |
| `NoQuote` | Balance known, no price data |
| `NoAccount` | Account not created (e.g., Solana reserve) |
| `Unreachable` | Network error |
| `NoAmount` | Coin is added to portfolio but no blockchain data available for it |
| `MissedDerivation` | Coin has no derivations — failed to obtain a blockchain network address |
All Value subtypes carry `sources: Sources` tracking data freshness per dimension: `networkSource`, `quoteSource`, `stakingBalanceSource`, and aggregated `total`.
## Network
**Location:** `domain/models``com.tangem.domain.models.network.Network`
Represents a blockchain network (e.g., Ethereum, Bitcoin). Contains network metadata: ID, name, currency symbol, derivation path, standard type (ERC20, TRC20, BEP20, etc.), and capabilities (token support, transaction extras, name resolving).
## NetworkStatus
**Location:** `domain/models``com.tangem.domain.models.network.NetworkStatus`
Blockchain balances for all tokens of a network at a specific address. Only `Verified` and `NoAccount` are cached.
| Value subtype | Description |
|---|---|
| `Verified` | Successful response from blockchain |
| `Unreachable` | Failed response from blockchain |
| `NoAccount` | Blockchain-specific status for chains that require a deposit to an address before it can be used |
| `MissedDerivation` | Derivation failed — no blockchain network address |
## QuoteStatus
**Location:** `domain/models``com.tangem.domain.models.quote.QuoteStatus`
Exchange rate between the app's selected fiat currency and a coin's currency.
## YieldBalance
**Location:** `domain/models``com.tangem.domain.models.staking.YieldBalance`
Staking yield balance for a specific `StakingID` (integrationId + address).
## TotalFiatBalance
**Location:** `domain/models``com.tangem.domain.models.TotalFiatBalance`
Aggregate fiat balance across all tokens. Sealed interface with three states: `Loading`, `Failed`, `Loaded(amount, source)`.
## TokenList
**Location:** `domain/models``com.tangem.domain.models.tokenlist.TokenList`
List of cryptocurrency tokens for display in portfolio. Sealed interface with subtypes:
- `GroupedByNetwork` — tokens grouped by `Network`, each group contains a list of `CryptoCurrencyStatus`
- `Ungrouped` — flat list of `CryptoCurrencyStatus`
- `Empty` — no tokens
All subtypes carry `totalFiatBalance: TotalFiatBalance` and `sortedBy: TokensSortType`.
## Account
**Location:** `domain/models``com.tangem.domain.models.account.Account`
Model representing a user account. Subtypes:
- **`Account.CryptoPortfolio`** — crypto portfolio with coins. All tokens in the account share the account's derivation (main account is an exception). Has a `DerivationIndex`: `0` for main account, `1..19` for secondary
- **`Account.Payment`** — account for Visa card integration
## AccountStatus
**Location:** `domain/models``com.tangem.domain.models.account.AccountStatus`
Model representing an account with balances. Has a similar structure to `Account`: `CryptoPortfolio` and `Payment` subtypes.
## AccountList
**Location:** `domain/account``com.tangem.domain.account.models.AccountList`
List of all accounts for a user wallet (`UserWallet`).
Business rules (enforced by factory returning `Either<Error, AccountList>`):
- Accounts list cannot be empty
- Exactly 1 main account
- Max 20 active accounts (`MAX_ACCOUNTS_COUNT`), max 1000 archived
- No duplicate AccountIds or custom AccountNames
- `totalAccounts >= activeAccounts`
## AccountStatusList
**Location:** `domain/account``com.tangem.domain.account.models.AccountStatusList`
Same as `AccountList` but with balances (wraps `AccountStatus` instead of `Account`).
## UserWallet
**Location:** `domain/models``com.tangem.domain.models.wallet.UserWallet`
Top-level model representing a user's wallet stored in the app. Subtypes:
- **`Cold`** — wallet backed by a physical Tangem card (NFC). Contains `ScanResponse`, card info, backup state
- **`Hot`** — software (hot) wallet without a physical card
## Model Hierarchy
```
UserWallet
└─ AccountList / AccountStatusList
└─ Account.CryptoPortfolio / AccountStatus.CryptoPortfolio
├─ CryptoCurrency (Coin | Token)
│ └─ CryptoCurrencyStatus (currency + Value state)
│ ├─ built from NetworkStatus (per network)
│ ├─ built from QuoteStatus (per rawCurrencyId)
│ └─ built from YieldBalance (per stakingId)
├─ AccountId (SHA-256 hash)
├─ DerivationIndex (0 = main)
└─ CryptoPortfolioIcon (icon + color)
```

View file

@ -0,0 +1,22 @@
# Git Rules
## Branch Naming
| Type | Format | Example |
|------|--------|---------|
| Feature | `feature/AND-xxx_short_description` | `feature/AND-13391_balance_fetcher` |
| Bugfix | `bugfix/AND-xxx_short_description` | `bugfix/AND-14000_fix_crash` |
| Pre-release | `x.x_pre_release` | `5.36_pre_release` |
**Key branches:**
- `develop` — main integration branch, all feature/bugfix branches merge here
- `x.x_pre_release` — branched from `develop` on the last day of sprint for the upcoming release; receives regression bugfixes and additional release items
- `release` — merging into this branch triggers appTester build and production artifacts; PRs come from `x.x_pre_release`
## Commit Messages
Format: `AND-xxx Description`
- Start with the Jira task number (AND-xxx)
- Followed by a space and a short description in English
- Example: `[REDACTED_TASK_KEY] Finalize CryptoCurrencyBalanceFetcher refactoring`

View file

@ -0,0 +1,16 @@
# Tangem SDK & Libraries
## In-house SDKs (via `tangem_dependencies.toml`)
- **Blockchain SDK** (`com.tangem:blockchain`) — multichain SDK for working with blockchains: creating/signing transactions, fetching balances, managing addresses. Wrapped in `libs/blockchain-sdk/`
- **Card SDK** (`com.tangem.tangem-sdk-kotlin:core`, `:android`) — SDK for interacting with physical Tangem cards via NFC: scanning, wallet creation, key derivation, passcode management, backup. Wrapped in `libs/tangem-sdk-api/`
- **Hot SDK** (`com.tangem.tangem-hot-sdk-kotlin:core`, `:android`) — SDK for hot (software) wallets
- **Vico** (`com.tangem.vico`) — forked charting library Vico, adapted for project needs
## Wrapper Modules (`libs/`)
- `libs/blockchain-sdk/` — wrapper around Blockchain SDK, provides domain-level abstractions for blockchain operations
- `libs/tangem-sdk-api/` — wrapper around Card SDK, exposes NFC card interaction API to the app
- `libs/crypto/` — cryptographic utilities: derivation, address handling, blockchain-specific helpers
- `libs/auth/` — API key provider interfaces for external services (Express, StakeKit)
- `libs/visa/` — Visa integration: smart contracts, limits, balances via Web3j

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.

114
CLAUDE.md Normal file
View file

@ -0,0 +1,114 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build & Test Commands
```bash
# Build debug APK (Google flavor)
./gradlew :app:assembleGoogleDebug
# Run all unit tests (debug/googleDebug variants + JVM modules)
./gradlew unitTest
# Run tests for a single module
./gradlew :features:wallet:impl:testDebugUnitTest # Android library module
./gradlew :app:testGoogleDebugUnitTest # App module
./gradlew :domain:tokens:test # Pure JVM module
# Run a single test class
./gradlew :core:ui:testDebugUnitTest --tests "com.tangem.core.ui.format.BigDecimalCryptoFormatTest"
# Detekt (static analysis) — runs automatically via convention plugin on applicable modules
./gradlew detekt detektMain
# Build UI tests APKs (for Marathon)
./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
# 1. :app:assembleGoogleMocked — main APK (mocked build)
# 2. :app:assembleGoogleMockedAndroidTest — test APK with instrumented tests
```
**Product flavors:** `google` and `huawei` (dimension: `service`). Default development flavor is `google`.
**Build types:** `debug`, `mocked`, `internal`, `external`, `release`.
## Architecture Overview
### Module Layers
The project is a heavily modularized Android app (~220 modules) organized in layers:
- **`app/`** — Application entry point, Hilt setup, navigation root
- **`domain/`** — Business logic and models. Each domain area (e.g., `tokens`, `wallets`, `card`) has a `models` submodule for pure data types and a core module for use cases
- **`data/`** — Repository implementations and data sources, mirrors domain structure
- **`features/`** — UI features using **API/Impl split pattern**: `features:foo:api` defines the public contract, `features:foo:impl` contains the implementation. This enforces clean dependency boundaries
- **`core/`** — Cross-cutting concerns: `ui`, `analytics`, `datasource`, `decompose`, `navigation`, `res`, `utils`, `security`, `pagination`
- **`common/`** — Shared models, routing, UI components, test utilities
- **`libs/`** — SDK wrappers: `blockchain-sdk`, `tangem-sdk-api`, `crypto`, `auth`, `visa`
### Component Architecture (Decompose)
The app uses [Decompose](https://github.com/arkivanov/Decompose) for lifecycle-aware components. Every feature screen follows this structure:
**API module** (`features/{name}/api/`):
- `{Name}Component` interface implementing `ComposableContentComponent`
- Inner `Params` data class for input parameters
- Inner `Factory` interface: `fun create(context: AppComponentContext, params: Params): {Name}Component`
**Impl module** (`features/{name}/impl/`):
- `Default{Name}Component` with `@AssistedInject` constructor taking `@Assisted appComponentContext: AppComponentContext` and `@Assisted params`
- Delegates `AppComponentContext by appComponentContext`
- Creates model via `getOrCreateModel(params)`
- `@Composable Content(modifier)` collects model state via `collectAsStateWithLifecycle()`
- Inner `@AssistedFactory` interface extending the public `Factory`
**Model** (`features/{name}/impl/.../model/`):
- `{Name}Model` extending `Model` base class, annotated `@ModelScoped`, uses `@Inject` constructor
- Receives params via `ParamsContainer.require<ParamsType>()`
- Exposes `StateFlow<{Name}UM>` (UM = UI Model, state class in `ui/state/` subpackage)
- Has `modelScope` (SupervisorJob + mainImmediate), auto-cancelled on destroy
**Child navigation within features:**
- `childStack()` — stacked screen navigation (back stack)
- `childSlot()` — optional overlays/bottom sheets (single or no child)
- `InnerRouter` — feature-internal navigation that delegates unknown routes to parent router
### Feature Package Conventions
- API package: `com.tangem.features.{name}.api` (plural `features`)
- Impl package: `com.tangem.feature.{name}.impl` (singular `feature` — legacy inconsistency, follow existing pattern per feature)
- Component: `{Name}Component` (api), `Default{Name}Component` (impl)
- Model: `{Name}Model` in `model/` subpackage
- UI State: `{Name}UM` in `ui/state/` subpackage
- UI Composable: in `ui/` subpackage
### Key Frameworks & Patterns
- **DI:** Hilt with `@SingletonComponent` scope and custom `@ModelScoped` scope for model-lifecycle dependencies
- **UI:** Jetpack Compose with Material3. Image loading via Coil
- **Navigation:** Custom `AppRouter` + `AppRoute` sealed classes with deep link support via `DeepLinkBuilder`
- **Networking:** Retrofit + Moshi for API communication
- **Local storage:** `AppPreferencesStore` for key-value pairs, `DataStore` for larger data
- **Async:** Kotlin Coroutines + Flow. Inject `CoroutineDispatcherProvider` (from `core/utils`) instead of using `Dispatchers.*` directly — provides `main`, `mainImmediate`, `io`, `default`, `single`
- **Error handling:** Arrow's `Either<Error, Success>` pattern throughout domain/data layers. `DataError` sealed hierarchy for domain errors. See `domain/core/CLAUDE.md` for the LCE pattern
- **Analytics:** `AnalyticsEvent(category, event, params)` in `core/analytics/models/`. Feature events are sealed class hierarchies extending `AnalyticsEvent`. Send via injected `AnalyticsEventHandler`
- **Feature toggles:** `FeatureTogglesManager` in `core/config-toggles/`. Toggles are defined in `core/config-toggles/src/main/assets/configs/feature_toggles_config.json` and auto-generated into a `FeatureToggles` enum by the convention plugin at build time. Each feature module exposes its own `XxxFeatureToggles` interface (in `api/`) with a `DefaultXxxFeatureToggles` implementation (in `impl/`) that delegates to `FeatureTogglesManager`
- **Supported languages:** `SupportedLanguages` in `core/utils/` defines the app's supported locales: en, ru, de, fr, it, ja, uk, zh, es. `getCurrentSupportedLanguageCode()` returns the device locale if supported, otherwise falls back to English. Used by API calls that accept a language parameter
### Build System
- **Gradle 8.14.1**, AGP 8.10.1, Kotlin 2.1.10
- **Version catalogs:** `gradle/dependencies.toml` (external/third-party dependencies) and `gradle/tangem_dependencies.toml` (in-house Tangem SDK dependencies)
- **Convention plugin:** `plugins/configuration/` — applies Detekt, configures test settings, generates environment configs and feature toggles
- **Custom Detekt rules:** `plugins/detekt-rules/`. Detekt configuration is in the `tangem-android-tools` git submodule. Key rule: `UnsafeStringResourceUsage` — prevents direct `stringResource()` / `pluralStringResource()` calls; use the `Safe`-suffixed variants instead
- **Localization:** Managed via [Lokalise](https://lokalise.com). Update strings by running `python3 lokalize.py`
- **GitHub Packages auth:** Requires `gpr.user` and `gpr.key` in `local.properties` for Tangem SDK dependencies
### Testing
- **JUnit 5** (Jupiter) for unit tests
- **MockK** for mocking
- **Turbine** for Flow testing
- **Truth** for assertions
- **Marathon** for UI tests (emulator-based, configured via `Marathonfile`)
- Shared test utilities in `common:test` and `test/core/`

View file

@ -161,6 +161,7 @@ dependencies {
implementation(projects.domain.news)
implementation(projects.domain.earn)
implementation(projects.domain.tokensync)
implementation(projects.domain.search)
implementation(projects.common)
implementation(projects.common.routing)
@ -216,6 +217,7 @@ dependencies {
implementation(projects.data.hotWallet)
implementation(projects.data.news)
implementation(projects.data.earn)
implementation(projects.data.search)
/** Features */
implementation(projects.features.referral.impl)
@ -369,9 +371,7 @@ dependencies {
implementation(deps.googlePlay.services)
implementation(deps.googlePlay.advertising)
coreLibraryDesugaring(deps.desugar)
implementation(deps.timber)
implementation(deps.kermit)
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.coil)
implementation(deps.coil.gif)
@ -390,6 +390,7 @@ dependencies {
implementation(deps.viewBindingDelegate)
implementation(deps.armadillo)
implementation(deps.kotlin.serialization)
implementation(deps.reKotlin)
implementation(deps.reownCore)
implementation(deps.reownWeb3)
implementation(deps.prettyLogger)

View file

@ -8,7 +8,7 @@ import dagger.hilt.android.testing.OnComponentReadyRunner
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.lang.reflect.Field
class ApplicationInjectionExecutionRule(
@ -46,7 +46,7 @@ class ApplicationInjectionExecutionRule(
try {
originalVersionValues = FeatureToggles.entries.associateWith { it.version }
} catch (e: Exception) {
Timber.e("Failed to save original toggles values: ${e.message}")
TangemLogger.e("Failed to save original toggles values: ${e.message}")
}
}
@ -60,9 +60,10 @@ class ApplicationInjectionExecutionRule(
versionField.set(toggle, newVersion)
}
Timber.i("FeatureToggles.values updated: $toggleStates")
TangemLogger.i("FeatureToggles.values updated: $toggleStates")
} catch (e: Exception) {
Timber.e("FeatureToggles.values didn't change with error: ${e.message}")
TangemLogger.e("FeatureToggles.values didn't change with error: ${e.message}")
}
}
@ -75,9 +76,9 @@ class ApplicationInjectionExecutionRule(
versionField.set(toggle, originalVersion)
}
Timber.i("FeatureToggles.values restored")
TangemLogger.i("FeatureToggles.values restored")
} catch (e: Exception) {
Timber.e("FeatureToggles.values didn't restored with error: ${e.message}")
TangemLogger.e("FeatureToggles.values didn't restored with error: ${e.message}")
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.common.extensions
import android.os.SystemClock
import androidx.compose.ui.test.ComposeTimeoutException
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
@ -61,9 +62,14 @@ fun KNode.clickAndWaitFor(
fun KNode.performTextInputInChunks(
text: String,
chunkSize: Int = 2
chunkSize: Int = 2,
delayBetweenChunksMs: Long = 100
) {
text.chunked(chunkSize).forEach { chunk ->
val chunks = text.chunked(chunkSize)
chunks.forEachIndexed { index, chunk ->
performTextInput(chunk)
if (index < chunks.lastIndex) {
SystemClock.sleep(delayBetweenChunksMs)
}
}
}

View file

@ -11,7 +11,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* A JUnit rule that sets up the API environment for tests based on annotations or instrumentation arguments.
@ -82,11 +82,11 @@ class ApiEnvironmentRule : TestRule {
val environment = ApiEnvironment.valueOf(parts[1])
apiConfigId to environment
} catch (e: IllegalArgumentException) {
Timber.w("Invalid config or environment: $configPair")
TangemLogger.w("Invalid config or environment: $configPair")
null
}
} else {
Timber.w("Invalid config format: $configPair. Expected format: 'ConfigId=Environment'")
TangemLogger.w("Invalid config format: $configPair. Expected format: 'ConfigId=Environment'")
null
}
}
@ -94,7 +94,7 @@ class ApiEnvironmentRule : TestRule {
DEFAULT_API_CONFIGS.associateWith { ApiEnvironment.MOCK } + parsedConfigs
} catch (e: Exception) {
Timber.w("Failed to parse environment configs: $envConfigArg")
TangemLogger.w("Failed to parse environment configs: $envConfigArg")
DEFAULT_API_CONFIGS.associateWith { ApiEnvironment.MOCK }
}
}
@ -115,7 +115,7 @@ class ApiEnvironmentRule : TestRule {
runBlocking {
targetEnvironments.forEach { (apiConfigId, environment) ->
changeEnvironment(apiConfigId.name, environment)
Timber.i("$apiConfigId environment set to: ${environment.name}")
TangemLogger.i("$apiConfigId environment set to: ${environment.name}")
}
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.common.utils
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.util.concurrent.TimeUnit
/**
@ -13,7 +13,7 @@ fun getWcUri(
network: String = "ethereum",
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Getting WC URI for network: $network")
TangemLogger.i("Getting WC URI for network: $network")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения
@ -29,31 +29,31 @@ fun getWcUri(
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
TangemLogger.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
TangemLogger.i("Response body: $body")
val jsonObject = JSONObject(body)
if (jsonObject.getBoolean("success")) {
val wcUri = jsonObject.getString("wcUri")
Timber.i("Got WC URI successfully: $wcUri")
TangemLogger.i("Got WC URI successfully: $wcUri")
wcUri
} else {
Timber.e("API returned error: ${jsonObject.optString("error", "Unknown")}")
TangemLogger.e("API returned error: ${jsonObject.optString("error", "Unknown")}")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error getting WC URI")
TangemLogger.e("Error getting WC URI", e)
null
}
}
@ -61,7 +61,7 @@ fun getWcUri(
fun checkServiceHealth(
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Checking service health")
TangemLogger.i("Checking service health")
val client = OkHttpClient()
val request = Request.Builder()
@ -71,14 +71,14 @@ fun checkServiceHealth(
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
TangemLogger.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
TangemLogger.i("Response body: $body")
if (body.isEmpty()) {
Timber.e("Response body is empty")
TangemLogger.e("Response body is empty")
return null
}
@ -86,20 +86,20 @@ fun checkServiceHealth(
val status = jsonObject.optString("status", "")
if (status.isNotEmpty()) {
Timber.i("Got status successfully: $status")
TangemLogger.i("Got status successfully: $status")
status
} else {
Timber.e("Status field is missing or empty")
TangemLogger.e("Status field is missing or empty")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error checking health")
TangemLogger.e("Error checking health", e)
null
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.utils.WireMockRedirectInterceptor
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.io.IOException
private const val DEFAULT_WIREMOCK_URL = "[REDACTED_ENV_URL]"
@ -27,8 +27,8 @@ fun setWireMockScenarioState(
state: String,
baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Set ===")
Timber.i("Setting scenario '$scenarioName' to state: $state")
TangemLogger.i("=== WireMock Scenario Set ===")
TangemLogger.i("Setting scenario '$scenarioName' to state: $state")
val client = OkHttpClient()
val json = """{"state": "$state"}"""
val mediaType = "application/json".toMediaType()
@ -41,14 +41,14 @@ fun setWireMockScenarioState(
return try {
client.newCall(request).execute().use { response ->
val body = response.body?.string() ?: ""
Timber.d("WireMock scenario request URL: ${request.url}")
Timber.d("WireMock scenario request body: $json")
Timber.d("WireMock scenario response: ${response.code} - ${response.message}")
Timber.d("WireMock scenario response body: $body")
TangemLogger.d("WireMock scenario request URL: ${request.url}")
TangemLogger.d("WireMock scenario request body: $json")
TangemLogger.d("WireMock scenario response: ${response.code} - ${response.message}")
TangemLogger.d("WireMock scenario response body: $body")
response.isSuccessful
}
} catch (e: IOException) {
Timber.e(e, "WireMock scenario error")
TangemLogger.e("WireMock scenario error", e)
false
}
}
@ -67,12 +67,12 @@ fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean {
return try {
client.newCall(request).execute().use { response ->
val body = response.body?.string() ?: ""
Timber.d("WireMock status check: ${response.code}")
Timber.d("Available scenarios: $body")
TangemLogger.d("WireMock status check: ${response.code}")
TangemLogger.d("Available scenarios: $body")
response.isSuccessful
}
} catch (e: IOException) {
Timber.e(e, "WireMock not accessible")
TangemLogger.e("WireMock not accessible", e)
false
}
}
@ -82,12 +82,12 @@ fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean {
* @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/
fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean {
Timber.i("=== WireMock Scenarios Reset ===")
Timber.i("Base URL: $baseUrl")
TangemLogger.i("=== WireMock Scenarios Reset ===")
TangemLogger.i("Base URL: $baseUrl")
val client = OkHttpClient()
val url = "$baseUrl/__admin/scenarios/reset"
Timber.i("Request URL: $url")
TangemLogger.i("Request URL: $url")
val request = Request.Builder()
.url(url)
@ -95,19 +95,19 @@ fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean {
.build()
return try {
Timber.d("Sending reset request...")
TangemLogger.d("Sending reset request...")
client.newCall(request).execute().use { response ->
Timber.d("Response code: ${response.code}")
Timber.d("Response message: ${response.message}")
TangemLogger.d("Response code: ${response.code}")
TangemLogger.d("Response message: ${response.message}")
val responseBody = response.body?.string() ?: ""
Timber.d("Response body: $responseBody")
TangemLogger.d("Response body: $responseBody")
val isSuccessful = response.isSuccessful
Timber.d("Is successful: $isSuccessful")
TangemLogger.d("Is successful: $isSuccessful")
isSuccessful
}
} catch (e: IOException) {
Timber.e(e, "Exception during reset")
TangemLogger.e("Exception during reset", e)
false
}
}
@ -124,7 +124,7 @@ fun resetWireMockScenarioState(
initialState: String = "Started",
baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Reset ===")
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")
TangemLogger.i("=== WireMock Scenario Reset ===")
TangemLogger.i("Resetting scenario '$scenarioName' to initial state: $initialState")
return setWireMockScenarioState(scenarioName, initialState, baseUrl)
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.*
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
@ -599,22 +600,22 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Reset Wiremock scenario: '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = true)
step("Pull to refresh and wait for buttons to become enabled") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG, intervalMs = 2_000) {
pullToRefresh(10)
waitForIdle()
assertActionButtonsForMultiCurrencyWallet(isEnabled = true)
}
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
step("Pull to refresh and wait for buttons to become disabled") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG, intervalMs = 2_000) {
pullToRefresh(10)
waitForIdle()
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
}
}
}

View file

@ -104,6 +104,7 @@ class RecentBlockTest : BaseTestCase() {
@Test
fun recentBlockTransactionHistoryDoesNotSupportedTest() {
val tokenName = "Polkadot"
val fullTokenName = "Polkadot Asset Hub"
val sendAmount = "1"
setupHooks(
@ -113,7 +114,7 @@ class RecentBlockTest : BaseTestCase() {
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
openSendScreen(tokenName = fullTokenName, mockState = tokenName)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {

View file

@ -27,6 +27,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
@ -98,6 +99,7 @@ class SendAddressScreenTest : BaseTestCase() {
}
}
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("4543")
@DisplayName("Send (address screen): check address field")
@Test

View file

@ -17,6 +17,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
@ -152,7 +153,11 @@ class SendConfirmScreenTest : BaseTestCase() {
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) }
}
step("Assert 'Addresses shimmer' is not displayed") {
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
}
step("Click on 'Next' button") {
waitForIdle()
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert primary amount = '$tokenAmount'") {
@ -237,11 +242,13 @@ class SendConfirmScreenTest : BaseTestCase() {
}
}
@Ignore("TODO: [REDACTED_JIRA]")
@AllureId("554")
@DisplayName("Send (Confirm screen): check fee warning")
@Test
fun checkFeeWarningTest() {
val tokenName = "Polkadot"
val fullTokenName = "Polkadot Asset Hub"
val tokenAmount = "0.1"
val warningTitle = getResourceString(R.string.send_fee_unreachable_error_title)
val warningMessageResId = R.string.send_fee_unreachable_error_text
@ -256,7 +263,7 @@ class SendConfirmScreenTest : BaseTestCase() {
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
openSendScreen(tokenName = fullTokenName, mockState = tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
@ -274,7 +281,11 @@ class SendConfirmScreenTest : BaseTestCase() {
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Assert 'Addresses shimmer' is not displayed") {
onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() }
}
step("Click on 'Next' button") {
waitForIdle()
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Network fee info unreachable' warning title is displayed") {

View file

@ -7,6 +7,7 @@ import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
@ -68,6 +69,7 @@ class SendFeeScreenTest : BaseTestCase() {
@Test
fun checkFeeBlockForFixedFeeTest() {
val tokenName = "Polkadot"
val fullTokenName = "Polkadot Asset Hub"
val tokenAmount = "1"
val feeAmount = "$0.05"
@ -78,7 +80,7 @@ class SendFeeScreenTest : BaseTestCase() {
}
).run {
step("Open 'Send' screen") {
openSendScreen(tokenName)
openSendScreen(tokenName = fullTokenName, mockState = tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
@ -93,10 +95,13 @@ class SendFeeScreenTest : BaseTestCase() {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
waitForIdle()
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed without fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false)
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false)
}
}
step("Click on 'Fee selector' block") {
onSendConfirmScreen { feeSelectorBlock.performClick() }
@ -161,7 +166,7 @@ class SendFeeScreenTest : BaseTestCase() {
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed with fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)

View file

@ -21,6 +21,7 @@ import org.junit.Test
@HiltAndroidTest
class PolkadotWarningsTest : BaseTestCase() {
private val tokenName = "Polkadot"
private val fullTokenName = "Polkadot Asset Hub"
private val amountToLeaveLessThanDeposit = "1.299"
private val amountToLeaveGreaterThanDeposit = "0.2"
private val depositAmount = "DOT 0.01"
@ -41,7 +42,7 @@ class PolkadotWarningsTest : BaseTestCase() {
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
openSendScreen(tokenName = fullTokenName, mockState = tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {

View file

@ -16,11 +16,13 @@ import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class SearchAndSwapTest : BaseTestCase() {
@Ignore("ToDo: [REDACTED_JIRA]")
@AllureId("8520")
@DisplayName("Search and Swap: add token without derivation")
@Test
@ -68,6 +70,7 @@ class SearchAndSwapTest : BaseTestCase() {
}
}
@Ignore("ToDo: [REDACTED_JIRA]")
@AllureId("8519")
@DisplayName("Search and Swap: add token with derivation")
@Test
@ -115,6 +118,7 @@ class SearchAndSwapTest : BaseTestCase() {
}
}
@Ignore("ToDo: [REDACTED_JIRA]")
@AllureId("8523")
@DisplayName("Search and Swap: Markets error")
@Test
@ -155,6 +159,7 @@ class SearchAndSwapTest : BaseTestCase() {
}
}
@Ignore("ToDo: [REDACTED_JIRA]")
@AllureId("8522")
@DisplayName("Search and Swap: check 'Unsupported token pair' warning")
@Test
@ -205,6 +210,7 @@ class SearchAndSwapTest : BaseTestCase() {
}
}
@Ignore("ToDo: [REDACTED_JIRA]")
@AllureId("8521")
@DisplayName("Swap: search token on Swap token screen")
@Test

View file

@ -1,9 +1,9 @@
package com.tangem.tap
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.logging.TangemLogger
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import kotlinx.coroutines.tasks.await
import timber.log.Timber
import javax.inject.Inject
internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider {
@ -11,7 +11,7 @@ internal class FirebasePushNotificationsTokenProvider @Inject constructor() : Pu
return try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
TangemLogger.e("Error", ex)
""
}
}

View file

@ -5,7 +5,7 @@ import com.google.android.gms.tasks.Task
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.core.navigation.review.ReviewManager
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import com.google.android.play.core.review.ReviewManager as GReviewManager
/**
@ -26,7 +26,7 @@ internal class GoogleReviewManager : ReviewManager {
onDismissClick = onDismissClick,
)
}
.addOnFailureListener(Timber::e)
.addOnFailureListener { TangemLogger.e("Error", it) }
}
}
@ -42,9 +42,9 @@ internal class GoogleReviewManager : ReviewManager {
.addOnCompleteListener { resultReviewTask ->
if (!resultReviewTask.isSuccessful) onDismissClick()
}
.addOnFailureListener(Timber::e)
.addOnFailureListener { TangemLogger.e("Error", it) }
} else {
Timber.e(task.exception)
TangemLogger.e("Error", task.exception)
}
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
internal class HuaweiPushNotificationsTokenProvider @Inject constructor(
@ -25,7 +25,7 @@ internal class HuaweiPushNotificationsTokenProvider @Inject constructor(
try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
TangemLogger.e("Error", ex)
""
}
} else {
@ -33,10 +33,10 @@ internal class HuaweiPushNotificationsTokenProvider @Inject constructor(
try {
val appId = AGConnectOptionsBuilder().build(context).getString(APP_ID_KEY)
val token = HmsInstanceId.getInstance(context).getToken(appId, TOKEN_REQUEST_MODE)
Timber.i("Requested token from HuaweiService: $token")
TangemLogger.i("Requested token from HuaweiService: $token")
token
} catch (e: ApiException) {
Timber.i("Fetching token from HuaweiService failed cause: ${e.message}")
TangemLogger.i("Fetching token from HuaweiService failed cause: ${e.message}")
""
}
}

View file

@ -5,7 +5,7 @@ import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
import com.tangem.google.GoogleServicesHelper
import com.tangem.tap.common.pushes.PushNotificationDelegate
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
class HuaweiPushService : HmsMessageService() {
@ -15,12 +15,12 @@ class HuaweiPushService : HmsMessageService() {
override fun onNewToken(token: String?, bundle: Bundle?) {
super.onNewToken(token, bundle)
Timber.i("HuaweiPushService: On new token from HuaweiService: $token")
TangemLogger.i("HuaweiPushService: On new token from HuaweiService: $token")
}
override fun onTokenError(e: Exception?, bundle: Bundle?) {
super.onTokenError(e, bundle)
Timber.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}")
TangemLogger.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}")
}
override fun onMessageReceived(message: RemoteMessage?) {

View file

@ -4,7 +4,7 @@ import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import kotlin.reflect.KClass
object ForegroundActivityObserver {
@ -14,7 +14,7 @@ object ForegroundActivityObserver {
val foregroundActivity: AppCompatActivity?
get() = activities.entries
.firstOrNull { entry ->
Timber.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}")
TangemLogger.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}")
entry.value.isDestroyed == false
}
?.value
@ -27,15 +27,15 @@ object ForegroundActivityObserver {
}
override fun onActivityResumed(activity: Activity) {
Timber.i("onActivityResumed ${activity::class}")
TangemLogger.i("onActivityResumed ${activity::class}")
if (activity is AppCompatActivity) {
Timber.i("onActivityResumed store activity")
TangemLogger.i("onActivityResumed store activity")
activities[activity::class] = activity
}
}
override fun onActivityDestroyed(activity: Activity) {
Timber.i("onActivityDestroyed")
TangemLogger.i("onActivityDestroyed")
activities.remove(activity::class)
}

View file

@ -6,9 +6,9 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
@HiltWorker
class LockTimerWorker @AssistedInject constructor(
@ -19,11 +19,11 @@ class LockTimerWorker @AssistedInject constructor(
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
Timber.i("onStart job")
TangemLogger.i("onStart job")
userWalletsListRepository.lockAllWallets().onRight {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
Timber.i("onStart job complete")
TangemLogger.i("onStart job complete")
return Result.success()
}

View file

@ -13,11 +13,11 @@ import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
@ -47,7 +47,7 @@ internal class LockUserWalletsTimer(
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
coroutineScope.launch {
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
Timber.i(
TangemLogger.i(
"""
Owner resumed
|- Need to open welcome screen: $shouldOpenWelcomeScreenOnResume
@ -68,7 +68,7 @@ internal class LockUserWalletsTimer(
}
override fun onStop(owner: LifecycleOwner) {
Timber.i("Owner stopped")
TangemLogger.i("Owner stopped")
delayJob = null
startTimerWorker()
@ -76,7 +76,7 @@ internal class LockUserWalletsTimer(
fun restart() {
if (delayJob == null) return
Timber.i(
TangemLogger.i(
"""
Timer restart
|- Duration millis: ${duration.inWholeMilliseconds}
@ -96,7 +96,7 @@ internal class LockUserWalletsTimer(
private fun start(log: Boolean = true) {
if (log) {
Timber.i(
TangemLogger.i(
"""
Timer start
|- Duration millis: ${duration.inWholeMilliseconds}

View file

@ -67,11 +67,11 @@ import com.tangem.tap.routing.utils.DeepLinkFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.utils.extensions.uriValidate
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
@ -176,11 +176,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("onCreate")
TangemLogger.i("onCreate")
// We need to call it before onCreate to prevent unnecessary activity recreation
installAppTheme()
val splashScreen = installSplashScreen()
TangemLogger.i("Splash screen installed")
enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(
@ -322,18 +323,18 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onStart() {
super.onStart()
Timber.i("onStart")
TangemLogger.i("onStart")
dialogManager.onStart(this)
}
override fun onStop() {
dialogManager.onStop()
super.onStop()
Timber.i("onStop")
TangemLogger.i("onStop")
}
override fun onDestroy() {
Timber.i("onDestroy")
TangemLogger.i("onDestroy")
// workaround: kill process when activity destroy to avoid state when lock() wallets
// and navigation to unlock screen was skipped because system kills activity but not process
if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) {
@ -366,6 +367,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) {
@ -432,8 +434,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private fun sendStakingUnsubmittedHashes() {
lifecycleScope.launch {
sendUnsubmittedHashesUseCase.invoke()
.onLeft { Timber.e(it.toString()) }
.onRight { Timber.d("Submitting hashes succeeded") }
.onLeft { TangemLogger.e(it.toString()) }
.onRight { TangemLogger.d("Submitting hashes succeeded") }
}
}

View file

@ -75,13 +75,13 @@ import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.rekotlin.Store
import timber.log.Timber
lateinit var store: Store<AppState>
@ -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
*/
@ -299,10 +283,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
store = createReduxStore()
Timber.i("APP STARTED")
TangemLogger.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
TangemLogger.i(featureTogglesManager.toString())
TangemLogger.i(excludedBlockchainsManager.toString())
}
initWithConfigDependency(environmentConfig = environmentConfig)
@ -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

@ -7,7 +7,7 @@ import androidx.lifecycle.LifecycleOwner
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.analytics.models.event.TechAnalyticsEvent.WindowObscured.ObscuredState
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal object WindowObscurationObserver : DefaultLifecycleObserver {
@ -36,7 +36,7 @@ internal object WindowObscurationObserver : DefaultLifecycleObserver {
}
if (isPartiallyObscured) {
Timber.d("Window is partially obscured")
TangemLogger.d("Window is partially obscured")
if (!isWindowPartiallyObscuredAlreadySent) {
analyticsEventHandler.send(
@ -50,7 +50,7 @@ internal object WindowObscurationObserver : DefaultLifecycleObserver {
val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
if (isFullyObscured) {
Timber.d("Window is partially or fully obscured")
TangemLogger.d("Window is partially or fully obscured")
if (!isWindowFullyObscuredAlreadySent) {
analyticsEventHandler.send(

View file

@ -3,20 +3,11 @@ package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.ScanFailsDialog
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.common.ui.SimpleOkDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class DialogManager : StoreSubscriber<GlobalState> {
@ -48,49 +39,12 @@ class DialogManager : StoreSubscriber<GlobalState> {
if (dialog != null) return
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(
context = context,
source = state.dialog.source,
onTryAgain = state.dialog.onTryAgain,
)
is StateDialog.NfcFeatureIsUnavailable -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.nfc_error_unavailable,
context = context,
)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,
)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(
context = context,
unfinishedBackupScanResponse = state.dialog.scanResponse,
)
is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(
state.dialog.messageRes,
state.dialog.currencyTitle,
state.dialog.currencySymbol,
state.dialog.networkName,
),
context = context,
)
is AppDialog.WalletAlreadyWasUsedDialog -> WalletAlreadyWasUsedDialog.create(
context = context,
onOk = state.dialog.onOk,
onSupport = state.dialog.onSupportClick,
onCancel = state.dialog.onCancel,
)
is AppDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle),
messageRes = state.dialog.messageRes,
context = context,
primaryButtonRes = state.dialog.primaryButtonRes,
primaryButtonAction = state.dialog.onOk,
)
else -> null
}
dialog?.show()

View file

@ -1,9 +1,9 @@
package com.tangem.tap.common.analytics
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.core.analytics.api.EventLogger
import timber.log.Timber
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.utils.logging.TangemLogger
class AnalyticsEventsLogger(
private val name: String,
@ -11,11 +11,11 @@ class AnalyticsEventsLogger(
) : EventLogger, ExceptionLogger {
override fun logEvent(event: String, params: Map<String, String>) {
Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
TangemLogger.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
}
override fun logException(error: Throwable, params: Map<String, String>) {
Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params)))
TangemLogger.e(jsonConverter.prettyPrint(PrintEventModel(name, "error", params)), error)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLinkListener
import com.appsflyer.deeplink.DeepLinkResult
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
import javax.inject.Singleton
@ -17,10 +17,10 @@ class AppsFlyerDeepLinkListener @Inject constructor(
referralParamsHandler.handle(deepLink = p0.deepLink)
}
DeepLinkResult.Status.NOT_FOUND -> {
Timber.i("No deep link found")
TangemLogger.i("No deep link found")
}
DeepLinkResult.Status.ERROR -> {
Timber.e("Deep link error: ${p0.error}")
TangemLogger.e("Deep link error: ${p0.error}")
}
}
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLink
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.contracts.ExperimentalContracts
@ -41,15 +41,15 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
if (deepLinkValue != REFERRAL_DEEP_LINK_VALUE) {
Timber.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
return
}
@Suppress("NullableToStringCall")
Timber.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
if (!isValidParam(deepLinkSub1)) {
Timber.e("Deeplink conversion data is invalid")
TangemLogger.e("Deeplink conversion data is invalid")
return
}
@ -69,7 +69,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
coroutineScope.launch {
mutex.withLock {
setShouldShowMobileWalletPromoUseCase(true)
.onLeft { Timber.e(it) }
.onLeft { TangemLogger.e("Error", it) }
appsFlyerStore.storeIfAbsent(
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
)

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.AppsFlyerConversionListener
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
import javax.inject.Singleton
@ -11,7 +11,7 @@ class TangemAFConversionListener @Inject constructor(
) : AppsFlyerConversionListener {
override fun onConversionDataSuccess(p0: Map<String?, Any?>?) {
Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
TangemLogger.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
if (p0 == null) return
@ -19,14 +19,14 @@ class TangemAFConversionListener @Inject constructor(
}
override fun onConversionDataFail(p0: String?) {
Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
TangemLogger.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
}
override fun onAppOpenAttribution(p0: Map<String?, String?>?) {
Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
TangemLogger.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
}
override fun onAttributionFailure(p0: String?) {
Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
TangemLogger.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
}
}

View file

@ -6,16 +6,16 @@ import com.appsflyer.attribution.AppsFlyerRequestListener
import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener
import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener
import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import timber.log.Timber
interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder
@ -40,9 +40,9 @@ class AppsFlyerClient @AssistedInject constructor(
init(apiKey, tangemAFConversionListener, context)
Timber.i("Starting AppsFlyer SDK")
TangemLogger.i("Starting AppsFlyer SDK")
start(context, apiKey, InitializationListener)
Timber.i("AppsFlyer SDK started")
TangemLogger.i("AppsFlyer SDK started")
saveUID()
}
@ -57,7 +57,7 @@ class AppsFlyerClient @AssistedInject constructor(
}
override fun logEvent(event: String, params: Map<String, String>) {
Timber.tag("AppsFlyer").i("Logging event: $event with params: $params")
TangemLogger.withTag("AppsFlyer").i("Logging event: $event with params: $params")
appsFlyerLib.logEvent(
context,
event,
@ -78,21 +78,21 @@ class AppsFlyerClient @AssistedInject constructor(
private object InitializationListener : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.d("AppsFlyer initialized successfully")
TangemLogger.d("AppsFlyer initialized successfully")
}
override fun onError(p0: Int, p1: String) {
Timber.e("AppsFlyer initialization error: $p0, $p1")
TangemLogger.e("AppsFlyer initialization error: $p0, $p1")
}
}
private object LogEventListener : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
TangemLogger.withTag("AppsFlyerClient").i("AppsFlyerRequestListener send")
}
override fun onError(p0: Int, p1: String) {
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
TangemLogger.withTag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
}
}

View file

@ -6,14 +6,14 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.AppsFlyerOnlyEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
) : AnalyticsHandler, AnalyticsUserIdHandler {
init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer Analytics Handler created")
}
override fun id(): String = ID
@ -21,11 +21,15 @@ class AppsFlyerAnalyticsHandler(
override fun send(event: AnalyticsEvent) {
when (event) {
is AppsFlyerOnlyEvent -> {
Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
TangemLogger.withTag(
"AppsFlyer",
).i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
client.logEvent(event.id, event.params)
}
is AppsFlyerIncludedEvent -> {
Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
TangemLogger.withTag(
"AppsFlyer",
).i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
val replacedEvent = event.appsFlyerReplacedEvent ?: event.event
client.logEvent(
event = AnalyticsEvent(category = event.category, event = replacedEvent).id,
@ -52,15 +56,15 @@ class AppsFlyerAnalyticsHandler(
) : AnalyticsHandlerBuilder {
init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
}
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler = AppsFlyerAnalyticsHandler(
client = if (data.logConfig.isAppsflyerLogEnabled) {
Timber.tag("AppsFlyer").i("AppsFlyer log enabled, mock client created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer log enabled, mock client created")
AppsFlyerLogClient(data.jsonConverter)
} else {
Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer log disabled, real client created")
appsFlyerClientFactory.create(apiKey = data.config.appsFlyerApiKey)
},
)

View file

@ -1,11 +1,11 @@
package com.tangem.tap.common.analytics.handlers.customerio
import android.app.Application
import com.tangem.utils.logging.TangemLogger
import io.customer.messagingpush.ModuleMessagingPushFCM
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOBuilder
import io.customer.sdk.data.model.Region
import timber.log.Timber
/**
* Real Customer.io SDK client.
@ -32,7 +32,7 @@ internal class CustomerIoClient(
.addCustomerIOModule(ModuleMessagingPushFCM())
.build()
Timber.d("CustomerIO SDK initialized")
TangemLogger.d("CustomerIO SDK initialized")
}
override fun setUserId(userId: String) {

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.handlers.customerio
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* Log client for Customer.io (used in debug mode).
@ -13,11 +13,11 @@ internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
override fun setUserId(userId: String) {
this.userId = userId
Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
}
override fun clearUserId() {
Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
this.userId = null
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
@ -13,7 +13,7 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
Firebase.analytics.appInstanceId
.addOnSuccessListener { continuation.resume(it) }
.addOnFailureListener {
Timber.w("Fail to get appInstanceId")
TangemLogger.w("Fail to get appInstanceId")
continuation.resume(null)
}
}
@ -22,7 +22,7 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
return try {
Firebase.analytics.appInstanceId.result
} catch (e: IllegalStateException) {
Timber.e(e, "getAppInstanceIdSync")
TangemLogger.e("getAppInstanceIdSync", e)
null
}
}

View file

@ -6,7 +6,7 @@ import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
import android.os.Build
import android.os.PersistableBundle
import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import android.content.ClipboardManager as AndroidClipboardManager
internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager {
@ -24,13 +24,13 @@ internal class DefaultClipboardManager(private val clipboardManager: AndroidClip
val clip = clipboardManager.primaryClip
if (clip == null || clip.itemCount == 0) {
Timber.d("Clipboard is empty")
TangemLogger.d("Clipboard is empty")
return default
}
val clipDescription = clipboardManager.primaryClipDescription
if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) {
Timber.d("Clipboard doesn't contain text")
TangemLogger.d("Clipboard doesn't contain text")
return default
}

View file

@ -1,12 +1,12 @@
package com.tangem.tap.common.clipboard
import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal object MockClipboardManager : ClipboardManager {
override fun setText(text: String, isSensitive: Boolean, label: String) {
Timber.w("Clipboard Manager not available")
TangemLogger.w("Clipboard Manager not available")
}
override fun getText(default: String?): String? = null

View file

@ -0,0 +1,56 @@
package com.tangem.tap.common.deeplink
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.utils.logging.TangemLogger
/**
* [DeeplinkLauncher] implementation that launches deep links as intents to the current Activity
* and opens web URLs in the browser via [UrlOpener].
*/
internal class DefaultDeeplinkLauncher(
private val context: Context,
private val urlOpener: UrlOpener,
) : DeeplinkLauncher {
override fun launch(link: String) {
val deeplinkUri = link.toUri()
when (deeplinkUri.scheme) {
DeepLinkScheme.Tangem.scheme,
DeepLinkScheme.WalletConnect.scheme,
-> launchDeepLink(deeplinkUri)
DeepLinkScheme.Https.scheme -> launchDeeplinkOrOpenBrowser(deeplinkUri, link)
else -> {
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri
""".trimIndent(),
)
}
}
}
private fun launchDeeplinkOrOpenBrowser(uri: Uri, link: String) {
val intent = createDeepLinkIntent(uri)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
urlOpener.openUrl(link)
}
}
private fun launchDeepLink(uri: Uri) {
context.startActivity(createDeepLinkIntent(uri))
}
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(context.packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}

View file

@ -10,8 +10,8 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.delay
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -31,7 +31,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try
Result.Success(wallet)
}
} catch (exception: Exception) {
Timber.e(exception)
TangemLogger.e("Error", exception)
val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {

View file

@ -9,10 +9,10 @@ import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.util.Logger
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.utils.logging.TangemLogger
import okhttp3.OkHttpClient
import timber.log.Timber
import coil.util.Logger as CoilLogger
private const val COIL_LOG_TAG = "COIL"
private const val COIL_MEMORY_CACHE_SIZE = 0.25
@ -22,7 +22,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
.apply {
if (!logEnabled) return@apply
logger(CoilTimberLogger())
logger(CoilKermitLogger())
okHttpClient {
OkHttpClient.Builder()
.addNetworkInterceptor(createNetworkLoggingInterceptor())
@ -48,14 +48,16 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
.build()
}
private class CoilTimberLogger : Logger {
private class CoilKermitLogger : CoilLogger {
override var level: Int = Log.DEBUG
private val logger = TangemLogger.withTag(COIL_LOG_TAG)
override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) {
with(Timber.tag(COIL_LOG_TAG)) {
if (throwable != null) e(throwable, message)
if (message != null) d(message)
if (throwable != null) {
logger.e(message ?: "<EMPTY>", throwable)
} else if (message != null) {
logger.d(message)
}
}
}

View file

@ -8,8 +8,8 @@ import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.orhanobut.logger.AndroidLogAdapter
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import timber.log.Timber
import java.util.regex.Pattern
import com.orhanobut.logger.Logger as PrettyLogger
@ -30,18 +30,9 @@ class TangemAppLoggerInitializer(
PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
}
Timber.plant(tree = createTimberTree())
Logger.setLogWriters(KermitLogWriter(::finalLogOutput))
}
private fun createTimberTree(): Timber.Tree {
return object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
finalLogOutput(priority = priority, tag = tag, message = message, t = t)
}
}
}
private fun finalLogOutput(priority: Int, tag: String?, message: String, t: Throwable?) {
if (IS_LOG_ENABLED) {
PrettyLogger.log(priority, tag, message, t)
@ -71,6 +62,8 @@ private class KermitLogWriter(
KermitLogWriter::class.java.name,
BaseLogger::class.java.name,
Logger::class.java.name,
TangemLogger::class.java.name,
TangemLogger.TaggedLogger::class.java.name,
)
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
@ -87,7 +80,7 @@ private class KermitLogWriter(
tag
} else {
/**
* like in [Timber.DebugTree.tag]
* like in [Logger.debugTree.tag]
*/
@Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause")
Throwable().stackTrace
@ -99,7 +92,7 @@ private class KermitLogWriter(
}
/**
* copy from [Timber.DebugTree.createStackElementTag]
* copy from [Logger.debugTree.createStackElementTag]
*/
@Suppress("MagicNumber")
private fun createStackElementTag(element: StackTraceElement): String? {
@ -120,7 +113,7 @@ private class KermitLogWriter(
private const val KERMIT_LOGGER_DEFAULT_TAG = ""
/**
* copy from [Timber.DebugTree.Companion]
* copy from [Logger.debugTree.Companion]
*/
private const val MAX_TAG_LENGTH = 23
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")

View file

@ -4,9 +4,9 @@ import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.utils.logging.TangemLogger
import dagger.hilt.android.AndroidEntryPoint
import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@ -22,7 +22,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
TangemLogger.d("New FCM token received: $token")
if (customerIoFeatureToggles.isFeatureEnabled) {
CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token)

View file

@ -1,41 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.redux.StateDialog
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
sealed class AppDialog : StateDialog {
data class SimpleOkDialogRes(
val headerId: Int,
val messageId: Int,
val args: List<String> = emptyList(),
val onOk: VoidCallback? = null,
) : AppDialog()
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit,
) : AppDialog() {
val messageRes: Int = R.string.token_details_hide_alert_message
val titleRes: Int = R.string.token_details_hide_alert_title
val primaryButtonRes: Int = R.string.token_details_hide_alert_hide
}
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String,
val networkName: String,
) : AppDialog() {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
data class WalletAlreadyWasUsedDialog(
val onOk: () -> Unit,
val onSupportClick: () -> Unit,
val onCancel: () -> Unit,
) : AppDialog()
}

View file

@ -5,7 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import org.rekotlin.Middleware
@ -23,7 +22,6 @@ data class AppState(
logMiddleware,
GlobalMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
BackupMiddleware().backupMiddleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.utils.logging.TangemLogger
import org.rekotlin.Middleware
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -9,7 +9,7 @@ import timber.log.Timber
val logMiddleware: Middleware<AppState> = { _, _ ->
{ nextDispatch ->
{ action ->
Timber.i("Dispatch action: ${action::class.java.simpleName}")
TangemLogger.i("Dispatch action: ${action::class.java.simpleName}")
nextDispatch(action)
}
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object SimpleAlertDialog {
fun create(
titleRes: Int? = null,
messageRes: Int? = null,
title: String? = null,
message: String? = null,
primaryButtonRes: Int = R.string.common_ok,
context: Context,
): AlertDialog {
return SimpleCancelableAlertDialog.create(
titleRes = titleRes,
messageRes = messageRes,
title = title,
message = message,
primaryButtonRes = primaryButtonRes,
secondaryButtonRes = null,
context = context,
)
}
}
object SimpleCancelableAlertDialog {
fun create(
titleRes: Int? = null,
messageRes: Int? = null,
title: String? = null,
message: String? = null,
primaryButtonRes: Int = R.string.common_ok,
secondaryButtonRes: Int? = R.string.common_cancel,
primaryButtonAction: () -> Unit = {},
secondaryButtonAction: () -> Unit = {},
context: Context,
): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(if (titleRes != null) context.getString(titleRes) else title)
setMessage(if (messageRes != null) context.getString(messageRes) else message)
setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() }
if (secondaryButtonRes != null) {
setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction() }
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object SimpleOkDialog {
fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog {
val message = if (dialog.args.isEmpty()) {
context.getString(dialog.messageId)
} else {
context.getString(dialog.messageId, *dialog.args.toTypedArray())
}
return AlertDialog.Builder(context).apply {
setTitle(context.getString(dialog.headerId))
setMessage(message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatchDialogHide()
dialog.onOk?.invoke()
}
}.create()
}
}

View file

@ -14,8 +14,8 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import timber.log.Timber
internal class CustomTabsUrlOpener : UrlOpener {
@ -55,7 +55,7 @@ internal class CustomTabsUrlOpener : UrlOpener {
customTabsIntent.launchUrl(context, url.toUri())
}
}.onFailure {
Timber.e(it)
TangemLogger.e("Error", it)
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.core
import co.touchlab.kermit.Logger
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.SupervisorJob
@ -28,7 +28,7 @@ internal class DefaultAppCoroutineScope @Inject constructor(
}
private fun logError(throwable: Throwable, coroutineName: String) {
Logger.withTag(tag).e(
TangemLogger.withTag(tag).e(
messageString = "CoroutineName $coroutineName",
throwable = throwable,
)

View file

@ -8,7 +8,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.foregroundActivityObserver
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* Implementation of email sender for Android
@ -21,7 +21,7 @@ internal class AndroidEmailSender : EmailSender {
val activity = foregroundActivityObserver.foregroundActivity
if (activity == null) {
Timber.e("Foreground activity not found")
TangemLogger.e("Foreground activity not found")
return
}
@ -50,7 +50,7 @@ internal class AndroidEmailSender : EmailSender {
ContextCompat.startActivity(activity, chooserIntent, null)
} catch (ex: Exception) {
Timber.e("Failed to send email: $ex")
TangemLogger.e("Failed to send email: $ex")
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.core.security
import com.dexprotector.rtc.RtcStatus
import com.tangem.security.DeviceSecurityInfoProvider
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
override val isRooted: Boolean
@ -16,7 +16,7 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
return try {
RtcStatus.getRtcStatus()
} catch (e: Throwable) {
Timber.e(e)
TangemLogger.e("Error", e)
null
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.local.logs.AppLogsStore
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* BlockchainSDK logger implementation
@ -16,7 +16,7 @@ internal class TangemBlockchainSDKLogger(
) : BlockchainSDKLogger {
override fun log(level: BlockchainSDKLogger.Level, message: String) {
Timber.d(message)
TangemLogger.d(message)
appLogsStore.saveLogMessage(tag = "BlockchainSDK_${level.name}", message)
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.LinkHandler
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
@ -49,6 +49,7 @@ internal interface UtilsModule {
@Provides
@Singleton
fun provideLinkHandler(appRouter: AppRouter): LinkHandler = LinkHandler(appRouter)
fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher =
DefaultDeeplinkLauncher(context, urlOpener)
}
}

View file

@ -19,10 +19,10 @@ import com.tangem.datasource.local.visa.VisaOtpData
import com.tangem.datasource.local.visa.hasSavedOTP
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.pins.SetUserCodeCommand
@ -31,11 +31,11 @@ import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.measureTimedValue
@ -94,7 +94,7 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
}
Timber.i("VisaCardActivationTask all time: ${timedResult.duration}")
TangemLogger.i("VisaCardActivationTask all time: ${timedResult.duration}")
return timedResult.value
}
@ -109,11 +109,11 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
}
Timber.i("AttestCardKeyCommand time: ${timedResult.duration}")
TangemLogger.i("AttestCardKeyCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("AttestCardKeyCommand success")
TangemLogger.i("AttestCardKeyCommand success")
processSignedAuthorizationChallenge(
signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(),
@ -122,7 +122,7 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("AttestCardKeyCommand failure ${result.error}")
TangemLogger.e("AttestCardKeyCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -206,15 +206,15 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("CreateWalletTask time: ${timedResult.duration}")
TangemLogger.i("CreateWalletTask time: ${timedResult.duration}")
when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
TangemLogger.i("CreateWalletTask success")
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
TangemLogger.e("CreateWalletTask failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -237,11 +237,11 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("GenerateOTPCommand time: ${timedResult.duration}")
TangemLogger.i("GenerateOTPCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("GenerateOTPCommand success")
TangemLogger.i("GenerateOTPCommand success")
otpStorage.saveOTP(
cardId = cardId,
data = VisaOtpData(result.data.rootOTP, result.data.rootOTPCounter),
@ -249,7 +249,7 @@ class VisaCardActivationTask @AssistedInject constructor(
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("GenerateOTPCommand failure ${result.error}")
TangemLogger.e("GenerateOTPCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -278,11 +278,11 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("SignHashCommand time: ${timedResult.duration}")
TangemLogger.i("SignHashCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("SignHashCommand success")
TangemLogger.i("SignHashCommand success")
handleSignedData(
dataToSign = dataToSign,
response = result.data,
@ -290,7 +290,7 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("SignHashCommand failure ${result.error}")
TangemLogger.e("SignHashCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -336,7 +336,7 @@ class VisaCardActivationTask @AssistedInject constructor(
return CompletionResult.Success(Unit)
}
Timber.i("Setting access code")
TangemLogger.i("Setting access code")
val task = SetUserCodeCommand.changeAccessCode(mode.accessCode)
@ -348,15 +348,15 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("SetUserCodeCommand time: ${timedResult.duration}")
TangemLogger.i("SetUserCodeCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("SetUserCodeCommand success")
TangemLogger.i("SetUserCodeCommand success")
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.i("SetUserCodeCommand failure ${result.error}")
TangemLogger.i("SetUserCodeCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}

View file

@ -41,7 +41,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UserWalletsListManagerModule {
internal object UserWalletsListRepositoryModule {
@Provides
@Singleton

View file

@ -14,9 +14,9 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.crypto.spec.SecretKeySpec
internal class DefaultUserWalletsSensitiveInformationRepository(
@ -123,7 +123,7 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
this@decodeToSensitiveInformation.decodeToString(throwOnInvalidSequence = true),
)
} catch (e: CharacterCodingException) {
Timber.e(e, "Unable to decode sensitive information")
TangemLogger.e("Unable to decode sensitive information", e)
null
}

View file

@ -10,19 +10,18 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.error.VisaCardScanError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyTask
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
@ -38,10 +37,10 @@ internal class VisaCardScanHandler @Inject constructor(
)
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan")
TangemLogger.i("Attempting to handle Visa card scan")
val card = session.environment.card ?: run {
Timber.e("Card is null")
TangemLogger.e("Card is null")
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
}
@ -68,12 +67,12 @@ internal class VisaCardScanHandler @Inject constructor(
}
private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet")
TangemLogger.i("Started handling authorization using Visa wallet")
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
TangemLogger.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError)
}
@ -82,14 +81,14 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Failure(it.tangemError)
}
Timber.i("Requesting challenge for wallet authorization")
TangemLogger.i("Requesting challenge for wallet authorization")
val challengeResponse = visaAuthRemoteDataSource.getCardWalletAuthChallenge(
cardId = card.cardId,
// This is the wallet public key, not the address and it's alright, as the API expects it in this format
cardWalletAddress = wallet.publicKey.toHexString(),
).getOrElse { error ->
Timber.i("Failed to get Access token for Wallet public key authorization")
TangemLogger.i("Failed to get Access token for Wallet public key authorization")
return CompletionResult.Failure(error.tangemError)
}
@ -103,7 +102,7 @@ internal class VisaCardScanHandler @Inject constructor(
val signature = signChallengeResult.data.walletSignature
val salt = signChallengeResult.data.salt
Timber.i("Challenge signed with Wallet public key")
TangemLogger.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse.toSignedChallenge(
@ -113,7 +112,9 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}")
TangemLogger.e(
"Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}",
)
CompletionResult.Failure(signChallengeResult.error)
}
}
@ -125,19 +126,19 @@ internal class VisaCardScanHandler @Inject constructor(
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(signedChallenge = signedChallenge)
.getOrElse { error ->
Timber.i("Failed to get Access token for Wallet public key authorization.")
TangemLogger.i("Failed to get Access token for Wallet public key authorization.")
return if (
error is VisaApiError.ProductInstanceIsNotActivated ||
error is VisaApiError.ProductInstanceNotFoundActivationRequired
) {
Timber.i("Proceeding with card authorization.")
TangemLogger.i("Proceeding with card authorization.")
handleCardAuthorization(cardWalletAddress = cardWalletAddress)
} else {
CompletionResult.Failure(error.tangemError)
}
}
Timber.i("Authorized using Wallet public key successfully")
TangemLogger.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
@ -148,27 +149,27 @@ internal class VisaCardScanHandler @Inject constructor(
): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
TangemLogger.i("Requesting authorization challenge to sign")
val challengeResponse = visaAuthRemoteDataSource.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
).getOrElse { error ->
Timber.e("Failed to get challenge for Card authorization. Plain error: ${error.errorCode}")
TangemLogger.e("Failed to get challenge for Card authorization. Plain error: ${error.errorCode}")
return CompletionResult.Failure(error.tangemError)
}
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
TangemLogger.i("Received challenge to sign: ${challengeResponse.challenge}")
val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge)
val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenged signed.")
TangemLogger.i("Challenged signed.")
signChallengeResult.data
}
is CompletionResult.Failure -> {
Timber.e(
TangemLogger.e(
"Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}",
)
return CompletionResult.Failure(signChallengeResult.error)
@ -181,7 +182,7 @@ internal class VisaCardScanHandler @Inject constructor(
salt = attestCardKeyResponse.salt.toHexString(),
),
).getOrElse { error ->
Timber.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
TangemLogger.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
return CompletionResult.Failure(error.tangemError)
}
@ -191,7 +192,7 @@ internal class VisaCardScanHandler @Inject constructor(
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse { error ->
Timber.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
TangemLogger.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
return CompletionResult.Failure(error.tangemError)
}

View file

@ -20,6 +20,7 @@ import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -29,7 +30,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("MemberNameEqualsClassName")
class DetailsMiddleware {
@ -228,7 +228,7 @@ class DetailsMiddleware {
)
}
.doOnFailure { error ->
Timber.e(error, "Unable to delete saved access codes")
TangemLogger.e("Unable to delete saved access codes", error)
}
}
}

View file

@ -10,6 +10,8 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.common.util.cardTypesResolver
@ -25,9 +27,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.details.ui.cardsettings.CardInfo
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -36,11 +36,10 @@ import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addIf
import com.tangem.wallet.R
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -56,6 +55,7 @@ internal class CardSettingsModel @Inject constructor(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val onboardingRepository: OnboardingRepository,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<CardSettingsComponent.Params>()
@ -115,12 +115,7 @@ internal class CardSettingsModel @Inject constructor(
if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) {
cardSettingsInteractor.initialize(scanResponse)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
uiMessageSender.send(Dialogs.wrongWalletTapped())
}
}
}
@ -244,7 +239,7 @@ internal class CardSettingsModel @Inject constructor(
when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {
Timber.e("Failed to change access code: ${result.error}")
TangemLogger.e("Failed to change access code: ${result.error}")
}
}
}

View file

@ -28,6 +28,7 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -35,7 +36,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -190,7 +190,7 @@ internal class ResetCardModel @Inject constructor(
resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight {
deleteSavedAccessCodesUseCase(cardId = primaryCardId)
val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse { error ->
Timber.e("Unable to delete user wallet: $error")
TangemLogger.e("Unable to delete user wallet: $error")
return@launch
}

View file

@ -43,13 +43,13 @@ import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@ -149,13 +149,14 @@ 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
}
}
private suspend fun fetchUserCountry() {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
TangemLogger.e("Unable to fetch the user country code $it")
}
}
@ -189,8 +190,8 @@ internal class MainViewModel @Inject constructor(
private suspend fun fetchStakingOptions() {
fetchStakingOptionsUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch staking options") }
.onRight { Timber.d("Staking options were fetched successfully") }
.onLeft { TangemLogger.e("Unable to fetch staking options: $it") }
.onRight { TangemLogger.d("Staking options were fetched successfully") }
}
private fun initializeOffRamp() {
@ -346,7 +347,9 @@ internal class MainViewModel @Inject constructor(
val keyboardId = keyboardValidator.getKeyboardId()
if (keyboardId != null) {
Timber.d("Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}")
TangemLogger.d(
"Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}",
)
analyticsEventHandler.send(
event = TechAnalyticsEvent.KeyboardIdentifier(
@ -356,7 +359,7 @@ internal class MainViewModel @Inject constructor(
),
)
} else {
Timber.e("Unable to get keyboard identifier")
TangemLogger.e("Unable to get keyboard identifier")
}
}
}
@ -370,7 +373,7 @@ internal class MainViewModel @Inject constructor(
refresh = true,
).fold(
ifLeft = { error ->
Timber.e(error)
TangemLogger.e("Error", error)
analyticsEventHandler.send(
StoriesEvents.Error(
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
@ -387,7 +390,7 @@ internal class MainViewModel @Inject constructor(
)
}
} catch (ex: Exception) {
Timber.e(ex)
TangemLogger.e("Error", ex)
analyticsEventHandler.send(
StoriesEvents.Error(
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
@ -412,7 +415,7 @@ internal class MainViewModel @Inject constructor(
associateAndUpdateWallets(applicationId = applicationId)
}
}
.onLeft(Timber::e)
.onLeft { TangemLogger.e("Error", it) }
}
private suspend fun associateAndUpdateWallets(applicationId: ApplicationId) {

View file

@ -1,14 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
sealed class BackupDialog : StateDialog {
data class UnfinishedBackupFound(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ConfirmDiscardingBackup(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
}

View file

@ -1,63 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.events.Onboarding.Finished
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState)) return
when (action) {
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.discardSavedBackup()
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Finished())
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
if (action.unfinishedBackupScanResponse != null) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
}
}
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction()
}
sealed class BackupAction : Action {
data object DiscardBackup : BackupAction()
data object DiscardSavedBackup : BackupAction()
data class ResumeFoundUnfinishedBackup(
val unfinishedBackupScanResponse: ScanResponse?,
) : BackupAction()
}

View file

@ -1,29 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object ConfirmDiscardingBackupDialog {
fun create(context: Context, unfinishedBackupScanResponse: ScanResponse? = null): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.welcome_interrupted_backup_discard_title)
setMessage(R.string.welcome_interrupted_backup_discard_message)
setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ ->
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(unfinishedBackupScanResponse))
}
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
store.dispatch(BackupAction.DiscardSavedBackup)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store
import com.tangem.wallet.R
object UnfinishedBackupFoundDialog {
fun create(context: Context, scanResponse: ScanResponse? = null): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_warning)
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse))
}
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse)))
}
setCancelable(false)
}.create()
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object WalletAlreadyWasUsedDialog {
fun create(context: Context, onOk: () -> Unit, onCancel: () -> Unit, onSupport: () -> Unit): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.security_alert_title)
setMessage(R.string.wallet_been_activated_message)
setPositiveButton(R.string.this_is_my_wallet_title) { dialog, _ ->
onOk()
dialog.dismiss()
}
setNeutralButton(R.string.common_cancel) { dialog, _ ->
onCancel()
dialog.dismiss()
}
setNegativeButton(R.string.alert_button_request_support) { dialog, _ ->
onSupport()
dialog.dismiss()
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -19,9 +19,9 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -42,13 +42,13 @@ class MoonPayService(
override suspend fun update() {
withIOContext {
Timber.i("Start updating")
TangemLogger.i("Start updating")
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> {
Timber.e(result.error, "Failed to load user status")
TangemLogger.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
@ -57,7 +57,7 @@ class MoonPayService(
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> {
Timber.e(result.error, "Failed to load currencies")
TangemLogger.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
@ -77,7 +77,7 @@ class MoonPayService(
)
}
Timber.i("Successfully updated")
TangemLogger.i("Successfully updated")
_initializationStatus.value = lceContent()
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}

View file

@ -7,10 +7,11 @@ 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
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.reflect.KClass
internal class ProxyAppRouter(
@ -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 {
Timber.e(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) {
Timber.i(message)
logger.i(message)
try {
block()
} catch (e: Throwable) {
Timber.e(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)))
Timber.w(errorMessage)
logger.w(errorMessage)
with(receiver = config.snackbarHandler ?: return) {
showSnackbar(

View file

@ -23,20 +23,25 @@ import com.tangem.core.decompose.navigation.getOrCreateTyped
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.android.create
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.hot.TangemHotSDKProxy
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.root.RootDetectedWarningComponent
import com.tangem.tap.routing.RootContent
import com.tangem.tap.routing.component.RoutingComponent
@ -45,11 +50,12 @@ import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.utils.ChildFactory
import com.tangem.tap.routing.utils.DeepLinkFactory
import com.tangem.tap.store
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultRoutingComponent @AssistedInject constructor(
@ -71,6 +77,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val trackingContextProxy: TrackingContextProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val backupServiceHolder: BackupServiceHolder,
) : RoutingComponent,
AppComponentContext by context,
SnackbarHandler {
@ -101,7 +108,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
try {
childFactory.createChild(route, childByContext(childContext))
} catch (e: Exception) {
Timber.e(e, "App Router Failed")
TangemLogger.e("App Router Failed", e)
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(exception = e, params = mapOf("Category" to "App Routing")),
)
@ -251,9 +258,71 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
}
private fun checkForUnfinishedBackup() {
if (DemoHelper.tryHandle { store.state }) return
componentScope.launch(dispatchers.main) {
val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
val scanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
messageSender.send(unfinishedBackupFoundDialog(scanResponse))
}
}
private fun unfinishedBackupFoundDialog(scanResponse: ScanResponse): DialogMessage = DialogMessage(
title = resourceReference(R.string.common_warning),
message = resourceReference(R.string.welcome_interrupted_backup_alert_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_alert_resume),
onClick = {
analyticsEventHandler.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
resumeUnfinishedBackup(scanResponse)
},
)
},
secondActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_alert_discard),
onClick = {
analyticsEventHandler.send(OnboardingEvent.Backup.CancelInterruptedBackup())
messageSender.send(confirmDiscardingBackupDialog(scanResponse))
},
)
},
)
private fun confirmDiscardingBackupDialog(scanResponse: ScanResponse): DialogMessage = DialogMessage(
title = resourceReference(R.string.welcome_interrupted_backup_discard_title),
message = resourceReference(R.string.welcome_interrupted_backup_discard_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_discard_resume),
onClick = { resumeUnfinishedBackup(scanResponse) },
)
},
secondActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_discard_discard),
onClick = { discardSavedBackup() },
)
},
)
private fun resumeUnfinishedBackup(scanResponse: ScanResponse) {
router.replaceAll(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
private fun discardSavedBackup() {
componentScope.launch(dispatchers.main) {
backupServiceHolder.backupService.get()?.discardSavedBackup()
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
analyticsEventHandler.send(Onboarding.Finished())
}
}

View file

@ -23,6 +23,7 @@ import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLi
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.extensions.uriValidate
import com.tangem.utils.logging.TangemLogger
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.transformLatest
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -62,7 +62,7 @@ internal class DeepLinkFactory @Inject constructor(
fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) {
lastDeepLink = deeplinkUri
Timber.i(
TangemLogger.i(
"""
Received deep link intent
|- Received URI: $deeplinkUri
@ -108,7 +108,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent)
DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri)
else -> {
Timber.i(
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri
@ -157,7 +157,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri)
else -> {
Timber.i(
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri

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)
}
}
}

View file

@ -29,7 +29,6 @@ import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
class DeepLinkFactoryTest {
@ -126,7 +125,6 @@ class DeepLinkFactoryTest {
every { mockedUri.port } returns 443 // Default HTTPS port
every { mockedUri.fragment } returns null // No fragment in this URI
Timber.uprootAll() // Disable Timber logging for tests
}
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -25,7 +25,6 @@ dependencies {
implementation(deps.firebase.messaging)
// end
implementation(deps.timber)
implementation(deps.arrow.core)

View file

@ -13,6 +13,7 @@ dependencies {
/* Core */
implementation(projects.core.decompose)
implementation(projects.core.configToggles)
implementation(projects.core.utils)
/* Domain */
implementation(projects.domain.qrScanning.models)
@ -30,7 +31,6 @@ dependencies {
/* Libs - Other */
api(deps.kotlin.serialization)
implementation(deps.androidx.core.ktx)
implementation(deps.timber)
/* Tests */
testImplementation(deps.test.junit)

View file

@ -1,35 +0,0 @@
package com.tangem.common.routing
import android.net.Uri
import timber.log.Timber
/**
* Routes in-app content links (deep links and external URLs)
* - `tangem://` scheme → parsed to AppRoute and pushed via AppRouter
* - `https://` / `http://` → opened in external browser via UrlOpener
* - Unknown scheme logged and ignored
*/
class LinkHandler(
private val appRouter: AppRouter,
) {
fun navigate(link: String) {
val uri = Uri.parse(link)
handleTangemDeepLink(uri)
}
private fun handleTangemDeepLink(uri: Uri) {
val route = parseDeepLinkToRoute(uri)
if (route != null) {
appRouter.push(route)
} else {
Timber.w("ContentLinkHandler: unrecognized tangem deep link: %s", uri)
}
}
@Suppress("UnusedParameter", "FunctionOnlyReturningConstant")
private fun parseDeepLinkToRoute(uri: Uri): AppRoute? {
// TODO [REDACTED_TASK_KEY] refactor deepling routing
return null
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.common.uri
import com.google.firebase.crashlytics.FirebaseCrashlytics
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import java.net.URI
/**
@ -22,7 +22,7 @@ object ExternalUrlValidator {
} catch (e: Exception) {
val exception = IllegalStateException("Failed to validate URI: $externalUri", e)
Timber.e(exception)
TangemLogger.e("Error", exception)
FirebaseCrashlytics.getInstance().recordException(exception)
false

View file

@ -8,23 +8,20 @@ import com.tangem.utils.converter.Converter
class AccountIconItemStateConverter(
val size: AccountIconSize = AccountIconSize.Default,
) : Converter<Account, CurrencyIconState.CryptoPortfolio> {
) : Converter<Account.CryptoPortfolio, CurrencyIconState.CryptoPortfolio> {
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
is Account.CryptoPortfolio -> when {
value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter(
char = value.accountName.toUM().value,
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
else -> CurrencyIconState.CryptoPortfolio.Icon(
resId = value.icon.value.getResId(),
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
override fun convert(value: Account.CryptoPortfolio): CurrencyIconState.CryptoPortfolio = when {
value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter(
char = value.accountName.toUM().value,
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
else -> CurrencyIconState.CryptoPortfolio.Icon(
resId = value.icon.value.getResId(),
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
}
}

View file

@ -19,23 +19,20 @@ class AccountPortfolioItemUMConverter(
private val isBalanceHidden: Boolean = false,
private val isEnabled: Boolean = true,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
) : Converter<Account, UserWalletItemUM> {
) : Converter<Account.CryptoPortfolio, UserWalletItemUM> {
override fun convert(value: Account): UserWalletItemUM {
return when (value) {
is Account.CryptoPortfolio -> with(value) {
UserWalletItemUM(
id = accountId.value,
name = accountName.toUM().value,
information = getInfo(account = this),
balance = getBalanceInfo(),
isEnabled = isEnabled,
endIcon = endIcon,
onClick = onClick,
imageState = getImageState(account = this),
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
override fun convert(value: Account.CryptoPortfolio): UserWalletItemUM {
return with(value) {
UserWalletItemUM(
id = accountId.value,
name = accountName.toUM().value,
information = getInfo(account = this),
balance = getBalanceInfo(),
isEnabled = isEnabled,
endIcon = endIcon,
onClick = onClick,
imageState = getImageState(account = this),
)
}
}

View file

@ -73,7 +73,7 @@ class AmountFieldChangeTransformer(
fiatValue = fiatValue,
isError = isCheckFailed,
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isExceedBalance -> resourceReference(R.string.common_insufficient_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)

View file

@ -62,6 +62,7 @@ fun AmountFieldV2(
onValuePastedTriggerDismiss: () -> Unit,
onCurrencyChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
reserveSpaceForError: Boolean = true,
) {
val decimalFormat = rememberDecimalFormat()
@ -120,12 +121,13 @@ fun AmountFieldV2(
AmountSecondary(
amountUM = amountUM,
onCurrencyChange = onCurrencyChange,
reserveSpaceForError = reserveSpaceForError,
)
}
}
@Composable
private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit) {
private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit, reserveSpaceForError: Boolean) {
Box(
modifier = Modifier
.fillMaxWidth()
@ -144,29 +146,33 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -
AmountFieldCurrencyInfo(
amountUM = amountUM,
onCurrencyChange = onCurrencyChange,
modifier = if (reserveSpaceForError) Modifier.padding(bottom = 16.dp) else Modifier,
)
AmountFieldError(
isError = amountUM.amountTextField.isError,
isWarning = amountUM.amountTextField.isWarning,
error = amountUM.amountTextField.error,
reserveSpaceForError = reserveSpaceForError,
modifier = Modifier
.align(BottomCenter)
.padding(top = 24.dp),
.align(BottomCenter),
)
}
}
}
@Composable
private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) {
private fun BoxScope.AmountFieldCurrencyInfo(
amountUM: AmountState.Data,
onCurrencyChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val isFiatAvailable = amountUM.amountTextField.fiatAmount.value != null
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
modifier = modifier
.align(TopCenter)
.padding(bottom = 16.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
@ -255,13 +261,14 @@ private fun AmountFieldError(
isError: Boolean,
isWarning: Boolean,
error: TextReference,
reserveSpaceForError: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isError || isWarning,
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier,
modifier = if (reserveSpaceForError) modifier.padding(top = 24.dp) else modifier,
label = "Error field appearance animation",
) {
val errorText = remember(this, error) { error }
@ -271,7 +278,13 @@ private fun AmountFieldError(
style = TangemTheme.typography.caption2,
color = color,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT),
modifier = if (reserveSpaceForError) {
Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT)
} else {
Modifier
.padding(top = 24.dp)
.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT)
},
)
}
}
@ -324,6 +337,7 @@ private fun AmountFieldV2_Preview(@PreviewParameter(AmountFieldV2PreviewProvider
onValueChange = {},
onValuePastedTriggerDismiss = { },
onCurrencyChange = {},
reserveSpaceForError = false,
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}

View file

@ -40,13 +40,13 @@ internal fun String.checkExceedBalance(
maxEnterAmount: EnterAmountBoundary,
amountTextField: AmountFieldModel,
): Boolean {
val currencyCryptoAmount = maxEnterAmount.amount ?: BigDecimal.ZERO
val currencyFiatAmount = maxEnterAmount.fiatAmount ?: BigDecimal.ZERO
val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals)
val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals)
return if (amountTextField.isFiatValue) {
val currencyFiatAmount = maxEnterAmount.fiatAmount ?: return false
fiatDecimal > currencyFiatAmount
} else {
val currencyCryptoAmount = maxEnterAmount.amount ?: return false
cryptoDecimal > currencyCryptoAmount
}
}

View file

@ -0,0 +1,98 @@
package com.tangem.common.ui.notifications
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.notifications.CloseableIconButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme
private const val GRADIENT_START_COLOR = 0xFF252934
private const val GRADIENT_END_COLOR = 0xFF12141E
private const val GRADIENT_OFFSET_X = 164f
private const val GRADIENT_OFFSET_Y = 39f
private const val GRADIENT_RADIUS = 82f
@Composable
fun CreatePaymentAccountNotification(
onClick: () -> Unit,
onCloseClick: () -> Unit,
@DrawableRes image: Int,
title: TextReference,
subtitle: TextReference,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(
brush = Brush.radialGradient(
colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)),
center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y),
radius = GRADIENT_RADIUS,
),
)
.clickable(onClick = onClick),
) {
Image(
modifier = Modifier.size(78.dp),
painter = painterResource(id = image),
contentDescription = null,
)
Column(
modifier = Modifier
.padding(start = 78.dp, top = 12.dp, end = 12.dp, bottom = 12.dp)
.align(Alignment.CenterStart),
) {
Text(
modifier = Modifier.padding(end = TangemTheme.dimens.size32),
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.constantWhite,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
CloseableIconButton(
onClick = onCloseClick,
modifier = Modifier.align(alignment = Alignment.TopEnd),
iconTint = TangemTheme.colors.icon.inactive,
)
}
}
@Preview(widthDp = 360)
@Composable
private fun CreatePaymentAccountNotification_Preview() {
ForceDarkTheme {
CreatePaymentAccountNotification(
onClick = {},
onCloseClick = {},
image = R.drawable.img_tangem_pay_visa_banner,
title = resourceReference(R.string.tangempay_onboarding_banner_title),
subtitle = resourceReference(R.string.tangempay_onboarding_banner_description),
)
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessage
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.common.wallets.error.UnlockWalletError
import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.Reason
@ -22,12 +23,7 @@ inline fun UnlockWalletError.handle(
when (this) {
UnlockWalletError.AlreadyUnlocked -> onAlreadyUnlocked()
UnlockWalletError.ScannedCardWalletNotMatched -> {
showMessage(
DialogMessage(
title = resourceReference(R.string.common_warning),
message = resourceReference(R.string.error_wrong_wallet_tapped),
),
)
showMessage(Dialogs.wrongWalletTapped())
}
UnlockWalletError.UserCancelled -> onUserCancelled()
UnlockWalletError.UserWalletNotFound -> {

View file

@ -17,7 +17,6 @@ dependencies {
kapt(deps.hilt.kapt)
/** Other libraries */
implementation(deps.timber)
/** Core modules */
implementation(projects.core.analytics.models)

View file

@ -8,8 +8,8 @@ import com.amplitude.experiment.ExperimentUser
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.launch
import timber.log.Timber
internal class AmplitudeABTestsManager(
val application: Application,
@ -21,7 +21,7 @@ internal class AmplitudeABTestsManager(
override fun init() {
if (::client.isInitialized) {
Timber.w("AB Tests manager already initialized, skipping")
TangemLogger.w("AB Tests manager already initialized, skipping")
return
}
@ -40,7 +40,7 @@ internal class AmplitudeABTestsManager(
val allVariants = client.all()
logAllVariants(allVariants)
} catch (exception: Exception) {
Timber.e(exception, "Failed to fetch AB test variants")
TangemLogger.e("Failed to fetch AB test variants", exception)
}
}
}
@ -69,23 +69,23 @@ internal class AmplitudeABTestsManager(
}
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
Timber.d("=".repeat(SEPARATOR_LENGTH))
Timber.d("AB Tests: Fetched ${allVariants.size} variants")
Timber.d("=".repeat(SEPARATOR_LENGTH))
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
TangemLogger.d("AB Tests: Fetched ${allVariants.size} variants")
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
if (allVariants.isEmpty()) {
Timber.d("No variants available")
TangemLogger.d("No variants available")
} else {
allVariants.entries.forEachIndexed { index, (key, variant) ->
Timber.d("[${index + 1}/${allVariants.size}] Key: $key")
Timber.d(" → Value: ${variant.value ?: "null"}")
Timber.d(" → Payload: ${variant.payload ?: "null"}")
Timber.d(" → Key: ${variant.key ?: "null"}")
Timber.d("-".repeat(SEPARATOR_LENGTH))
TangemLogger.d("[${index + 1}/${allVariants.size}] Key: $key")
TangemLogger.d(" → Value: ${variant.value ?: "null"}")
TangemLogger.d(" → Payload: ${variant.payload ?: "null"}")
TangemLogger.d(" → Key: ${variant.key ?: "null"}")
TangemLogger.d("-".repeat(SEPARATOR_LENGTH))
}
}
Timber.d("=".repeat(SEPARATOR_LENGTH))
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
}
private companion object {

View file

@ -73,7 +73,6 @@ dependencies {
/** Other libraries */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
ksp(deps.moshi.kotlin.codegen)
/** Core modules */

View file

@ -1,7 +1,7 @@
package com.tangem.core.configtoggle.version
import androidx.annotation.VisibleForTesting
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* Presentation of application version (<major>.<minor>.<fix?>).
@ -67,7 +67,7 @@ internal class Version private constructor(value: String) : Comparable<Version>
return try {
Version(value)
} catch (exception: Exception) {
Timber.e(exception, "Invalid version - %s", value)
TangemLogger.e("Invalid version - $value", exception)
return null
}
}

30
core/datasource/CLAUDE.md Normal file
View file

@ -0,0 +1,30 @@
# core/datasource
## API Integration Guide
### Config Structure
- `ApiConfig` — base API config with `id` (`ApiConfig.ID`), `defaultEnvironment` (`ApiEnvironment`), and `environmentConfigs` (list of `ApiEnvironmentConfig`)
- `ApiEnvironmentConfig` — per-environment settings: `environment`, `baseUrl`, and `headers` (map of header name to `Provider<String>`)
### Config Management
- `ApiConfigsManager` — DI-available component for accessing configs via `getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig`
- Two implementations: `ProdApiConfigsManager` (release) and `DevApiConfigsManager` (extends `MutableApiConfigsManager`, used when `BuildConfig.TESTER_MENU_ENABLED`)
- `MutableApiConfigsManager` allows runtime environment switching via Tester Menu without app restart
### Adding a New API
1. Create `ApiConfig` subclass in `com.tangem.datasource.api.common.config` — override `defaultEnvironment` and `environmentConfigs`. DI dependencies can be injected via constructor
2. Register the new config ID in `ApiConfig.initializeId(...)`
3. Provide the config in `ApiConfigsModule` using `@Provides @IntoSet`
4. Provide the API Retrofit service in `NetworkModule`:
- Get environment config: `apiConfigsManager.getEnvironmentConfig(id)`
- Use `environmentConfig.baseUrl` for Retrofit base URL
- Apply headers via `OkHttpClient.Builder().applyApiConfig(id, apiConfigsManager)`
### Testing
- Add the new config to `API_CONFIGS` list in `ProdApiConfigsManagerTest`
- Add a test model in the `data` method with expected `ApiEnvironmentConfig` values
- If the config has constructor dependencies, mock them and set up behavior in `setup()`

View file

@ -97,7 +97,6 @@ dependencies {
implementation(deps.kotlin.datetime)
/** Logging */
implementation(deps.timber)
/** Network */
implementation(deps.moshi)

View file

@ -1,12 +1,12 @@
package com.tangem.datasource.api.common.response
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.utils.logging.TangemLogger
import okhttp3.Request
import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
internal class ApiResponseCallDelegate<T : Any>(
private val wrappedCall: Call<T>,
@ -41,10 +41,10 @@ internal class ApiResponseCallDelegate<T : Any>(
val error = try {
t.toApiError()
} catch (e: ApiResponseError) {
Timber.e(e, "error map toApiError")
TangemLogger.e("error map toApiError", e)
e
} catch (e: Exception) {
Timber.e(e, "onFailure UnknownException")
TangemLogger.e("onFailure UnknownException", e)
ApiResponseError.UnknownException(e)
}

View file

@ -2,9 +2,9 @@ package com.tangem.datasource.api.common.response
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.TimeoutCancellationException
import retrofit2.Response
import timber.log.Timber
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
@ -31,7 +31,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
ApiResponseError.HttpException(code, message(), errorBody)
}
} catch (e: Exception) {
Timber.e(e, "UnknownException occured")
TangemLogger.e("UnknownException occured", e)
ApiResponseError.UnknownException(e)
}

View file

@ -70,6 +70,7 @@ interface TangemExpressApi {
@Query("refundExtraId") refundExtraId: String?, // for cex only
@Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send
@Query("toExtraId") toExtraId: String?, // swap-and-send memo
@Query("quoteId") quoteId: String?, // swap-and-send memo
): ApiResponse<ExchangeDataResponse>
@GET("exchange-status")

View file

@ -25,4 +25,7 @@ data class ExchangeQuoteResponse(
@Json(name = "minAmount")
val minAmount: BigDecimal,
@Json(name = "quoteId")
val quoteId: String? = null,
)

Some files were not shown because too many files have changed in this diff Show more