diff --git a/.claude/docs/navigation-graph.md b/.claude/docs/navigation-graph.md new file mode 100644 index 0000000000..9e5202e1ca --- /dev/null +++ b/.claude/docs/navigation-graph.md @@ -0,0 +1,471 @@ +# Navigation Graph + +Complete navigation map of the app based on `AppRoute` sealed class and feature-internal routes. + +## 1. All AppRoute Paths + +58 top-level routes defined in `common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt`. + +| # | Route | Path | Description | +|---|-------|------|-------------| +| 1 | `Initial` | `/initial` | App entry point (splash) | +| 2 | `Home` | `/home` | Stories/home screen with launch mode | +| 3 | `Welcome` | `/welcome` | Welcome screen for returning users | +| 4 | `Disclaimer` | `/disclaimer` | Terms of service / disclaimer | +| 5 | `Wallet` | `/wallet` | Main wallet portfolio screen | +| 6 | `CurrencyDetails` | `/currency_details/{walletId}/{currencyId}` | Token/coin detail screen | +| 7 | `Send` | `/send/{walletId}/{currencyId}` | Send cryptocurrency | +| 8 | `Details` | `/details/{walletId}` | Wallet details / settings hub | +| 9 | `DetailsSecurity` | `/details/security` | Security mode settings | +| 10 | `Usedesk` | `/usedesk/{walletId}` | Customer support (Usedesk) | +| 11 | `CardSettings` | `/card_settings/{walletId}` | Card-specific settings | +| 12 | `AppSettings` | `/app_settings` | Global app settings | +| 13 | `ResetToFactory` | `/reset_to_factory/{walletId}/{cardId}/...` | Factory reset flow | +| 14 | `AccessCodeRecovery` | `/access_code_recovery` | Access code recovery | +| 15 | `ManageTokens` | `{source}/manage_tokens/{accountId}` | Add/remove tokens in portfolio | +| 16 | `ChooseManagedTokens` | `/{source}/choose_managed_tokens/...` | Token chooser for send-via-swap | +| 17 | `WalletConnectSessions` | `/wallet_connect_sessions` | WalletConnect sessions list | +| 18 | `QrScanning` | `/{source}/qr_scanning` | QR code scanner | +| 19 | `ReferralProgram` | `/referral_program` | Referral program | +| 20 | `Swap` | `/swap/{fromId}/{toId}/{walletId}/...` | Token swap screen | +| 21 | `AppCurrencySelector` | `/app_currency_selector` | Fiat currency selector | +| 22 | `Staking` | `/staking/{walletId}/{currencyId}/{integrationId}` | Staking screen | +| 23 | `PushNotification` | `/push_notification` | Push notification opt-in | +| 24 | `WalletSettings` | `/wallet_settings/{walletId}` | Per-wallet settings | +| 25 | `WalletBackup` | `/wallet_backup/{walletId}/{coldOption}` | Wallet backup options | +| 26 | `WalletHardwareBackup` | `/wallet_hardware_backup/{walletId}` | Hardware wallet backup | +| 27 | `Markets` | `/markets` | Markets token list | +| 28 | `MarketsTokenDetails` | `/markets_token_details/{tokenId}/{showPortfolio}` | Market token detail | +| 29 | `Onramp` | `/onramp/{walletId}/{symbol}` | Buy crypto (onramp) | +| 30 | `OnrampSuccess` | `/onramp/success/{txId}` | Onramp success screen | +| 31 | `BuyCrypto` | `/buy_crypto/{walletId}` | Buy crypto token selector | +| 32 | `SellCrypto` | `/sell_crypto/{walletId}` | Sell crypto token selector | +| 33 | `SwapCrypto` | `/swap_crypto/{walletId}` | Swap crypto token selector | +| 34 | `Onboarding` | `/onboarding_v2/{mode}` | Onboarding flow (v2) | +| 35 | `Stories` | `/stories$storyId` | Stories / promotional content | +| 36 | `NFT` | `/nft/{walletId}` | NFT collection list | +| 37 | `NFTSend` | `/send/nft/{walletId}/{collection}/{assetId}` | Send NFT | +| 38 | `CreateWalletSelection` | `/create_wallet_selection` | Choose wallet creation type | +| 39 | `CreateWalletStart` | `/create_wallet_start` | Wallet creation intro (cold/hot) | +| 40 | `CreateHardwareWallet` | `/create_hardware_wallet` | Create hardware wallet flow | +| 41 | `CreateMobileWallet` | `/create_mobile_wallet` | Create mobile (hot) wallet | +| 42 | `UpgradeWallet` | `/upgrade_wallet/{walletId}` | Upgrade hot wallet to hardware | +| 43 | `AddExistingWallet` | `/add_existing_wallet` | Import existing wallet | +| 44 | `WalletActivation` | `/wallet_activation/{walletId}` | Activate wallet post-creation | +| 45 | `CreateWalletBackup` | `/create_wallet_backup/{walletId}` | Backup flow for created wallet | +| 46 | `UpdateAccessCode` | `/update_access_code/{walletId}` | Change access code | +| 47 | `ViewPhrase` | `/view_seed_phrase/{walletId}` | View recovery phrase | +| 48 | `ForgetWallet` | `/forget_wallet/{walletId}` | Remove wallet from app | +| 49 | `SendEntryPoint` | `/send_entry_point/{walletId}/{currencyId}` | Send entry with swap option | +| 50 | `CreateAccount` | `/create_account/{walletId}` | Create new account | +| 51 | `EditAccount` | `/edit_account/{accountId}` | Edit account | +| 52 | `AccountDetails` | `/account_details/{accountId}` | Account details screen | +| 53 | `ArchivedAccountList` | `/archived_account/{walletId}` | Archived accounts list | +| 54 | `TangemPayDetails` | `/tangem_pay_details/{walletId}` | Tangem Pay card details | +| 55 | `TangemPayOnboarding` | `/tangem_pay_onboarding/{mode}` | Tangem Pay onboarding | +| 56 | `Kyc` | `/kyc` | KYC verification | +| 57 | `YieldSupplyEntry` | `/yield_supply_entry/{walletId}/{symbol}` | Yield/supply entry point | +| 58 | `NewsDetails` | `/news_details/{newsId}` | News article detail | + +## 2. Navigation Edges + +Each entry shows: **Source route** → target routes it can navigate to (via `push` or `replaceAll`). + +### Initial / Bootstrap + +| Source | Target | Method | Trigger | +|--------|--------|--------|---------| +| `Initial` | `Home`, `Welcome`, `Disclaimer`, `Onboarding`, etc. | `replaceAll` | App startup (DefaultRoutingComponent) | + +### Home + +| Target | Method | Trigger | +|--------|--------|---------| +| `ManageTokens(STORIES)` | push | After scan, manage tokens | +| `CreateWalletStart` | push | Create wallet from home | +| `Wallet` | replaceAll | After wallet saved / already saved | + +### Welcome + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateWalletSelection` | push | "Add new wallet" button | +| `Home()` | replaceAll | When wallets list becomes empty | +| `Wallet` | replaceAll | After scan / wallet unlock / biometric | + +### Disclaimer + +| Target | Method | Trigger | +|--------|--------|---------| +| `PushNotification(Stories)` | push | After accepting TOS (stories flow) | +| `Home()` | replaceAll | After accepting TOS (non-stories flow) | + +### Wallet (main portfolio) + +| Target | Method | Trigger | +|--------|--------|---------| +| `Details` | push | Open wallet details | +| `ManageTokens(ACCOUNT)` | push | Manage tokens for account | +| `Onboarding` | push | Continue backup / onboarding | +| `CurrencyDetails` | push | Tap on a token | +| `Home` | push | Open stories | +| `NFT` | push | Open NFT collection | +| `TangemPayOnboarding` | push | Tangem Pay banner | +| `TangemPayDetails` | push | Tangem Pay card details | +| `YieldSupplyEntry` | push | Yield supply action | +| `QrScanning(MainScreen)` | push | QR scanner | +| `Send` | push | Send from QR / action | +| `WalletBackup` | push | Backup warning banner | + +### CurrencyDetails (token details) + +| Target | Method | Trigger | +|--------|--------|---------| +| `Onramp` | push | Buy action | +| `SendEntryPoint` | push | Send action | +| `Swap` | push | Swap action | +| `CurrencyDetails` | push | Navigate to related token (from staking router) | +| `Staking` | push | Open staking (from token details router) | + +### Details (wallet details hub) + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateWalletSelection` | push | Add new wallet | +| `WalletSettings` | push | Open wallet settings | +| `WalletConnectSessions` | push | WalletConnect item | +| `AppSettings` | push | App settings item | +| `Disclaimer(isTosAccepted=true)` | push | View TOS | +| `Usedesk` | push | Customer support | +| `TangemPayOnboarding(FromBannerInSettings)` | push | Tangem Pay banner | + +### WalletSettings + +| Target | Method | Trigger | +|--------|--------|---------| +| `ReferralProgram` | push | Referral program | +| `WalletHardwareBackup` | push | Hardware backup | +| `CardSettings` | push | Card settings | +| `ForgetWallet` | push | Delete/forget wallet | +| `ViewPhrase` | push | View seed phrase | +| `AccountDetails` | push | Open account details | +| `ArchivedAccountList` | push | View archived accounts | +| `CreateAccount` | push | Create new account | +| `Home()` | replaceAll | After wallet deletion completes | + +### WalletBackup + +| Target | Method | Trigger | +|--------|--------|---------| +| `WalletActivation` | push | Start activation (no backup) | +| `ViewPhrase` | push | View phrase option | +| `WalletHardwareBackup` | push | Hardware backup option | + +### WalletHardwareBackup + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateHardwareWallet` | push | Create new hardware wallet | +| `UpgradeWallet` | push | Upgrade current hot wallet | +| `CreateWalletBackup` | push | Backup existing wallet | + +### CreateWalletSelection + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateMobileWallet` | push | Choose mobile wallet | +| `CreateHardwareWallet` | push | Choose hardware wallet | + +### CreateWalletStart + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateMobileWallet` | push | Create mobile wallet | +| `Wallet` | replaceAll | After wallet creation completes | + +### CreateMobileWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `AddExistingWallet` | push | Import existing wallet | +| `Wallet` | replaceAll | After creation completes | + +### CreateHardwareWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Wallet` | replaceAll | After hardware wallet created | + +### AddExistingWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Wallet` | replaceAll | After import completes | + +### UpgradeWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Onboarding(UpgradeHotWallet)` | push | Start upgrade onboarding | + +### CreateWalletBackup + +| Target | Method | Trigger | +|--------|--------|---------| +| `UpgradeWallet` | push | After backup, continue to upgrade | + +### AccountDetails + +| Target | Method | Trigger | +|--------|--------|---------| +| `EditAccount` | push | Edit account | + +### ForgetWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Home()` | replaceAll | After wallet forgotten | + +### WalletConnectSessions + +| Target | Method | Trigger | +|--------|--------|---------| +| `QrScanning(WalletConnect)` | push | Scan WC QR code | + +### Onboarding + +| Target | Method | Trigger | +|--------|--------|---------| +| `Home()` | replaceAll | Onboarding completed (no wallets) | +| `Wallet` | replaceAll | Onboarding completed (has wallets) | + +### PushNotification + +| Target | Method | Trigger | +|--------|--------|---------| +| `Home()` | replaceAll | After push notification opt-in (via nextRoute param) | + +### Staking + +| Target | Method | Trigger | +|--------|--------|---------| +| `CurrencyDetails` | push | Back to token details | + +### TangemPayDetails + +| Target | Method | Trigger | +|--------|--------|---------| +| `Swap` | push | Top up / withdraw via swap | + +### NFT + +| Target | Method | Trigger | +|--------|--------|---------| +| `NFTSend` | push | Send NFT | + +### Send (notifications) + +| Target | Method | Trigger | +|--------|--------|---------| +| `CurrencyDetails` | push | Navigate to fee token | + +### SwapCrypto / BuyCrypto / SellCrypto + +| Target | Method | Trigger | +|--------|--------|---------| +| `Swap` | push | After token selection (SwapCrypto) | +| `Onramp` | push | After token selection (BuyCrypto/SellCrypto) | + +### Deep Link Handlers (push to AppRoute) + +| Handler | Target Route | +|---------|-------------| +| `OnrampDeepLinkHandler` | Processes onramp callback params | +| `SellRedirectDeepLinkHandler` | `Send` (with sell redirect params) | +| `BuyDeepLinkHandler` | `BuyCrypto` | +| `SellDeepLinkHandler` | `SellCrypto` | +| `SwapDeepLinkHandler` | `SwapCrypto` | +| `ReferralDeepLinkHandler` | Referral handling | +| `WalletDeepLinkHandler` | Wallet handling | +| `TokenDetailsDeepLinkHandler` | `CurrencyDetails` | +| `StakingDeepLinkHandler` | `Staking` | +| `MarketsDeepLinkHandler` | `Markets` | +| `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` | +| `WalletConnectDeepLinkHandler` | WalletConnect pairing | +| `PromoDeeplinkHandler` | Promo handling | +| `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` | +| `NewsDetailsDeepLinkHandler` | `NewsDetails` | + +## 3. Nested Routes (Feature-Internal Navigation) + +### OnboardingRoute +**File:** `features/onboarding-v2/impl/.../routing/OnboardingRoute.kt` + +| Route | Description | +|-------|-------------| +| `None` | Initial empty state | +| `Note` | Single-card onboarding note | +| `MultiWallet` | Multi-wallet onboarding (with seed phrase flow option) | +| `Visa` | Visa card onboarding | +| `Twins` | Twin cards onboarding | +| `ManageTokens` | Token management during onboarding | +| `AskBiometry` | Biometry setup prompt | +| `Done` | Onboarding completion | + +### WalletRoute +**File:** `features/wallet/impl/.../navigation/WalletRoute.kt` + +| Route | Description | +|-------|-------------| +| `Wallet` | Main wallet view | +| `OrganizeTokens` | Reorder tokens in portfolio | + +### SendEntryRoute +**File:** `features/send-v2/api/.../entry/SendEntryRoute.kt` + +| Route | Description | +|-------|-------------| +| `Send` | Direct send flow | +| `SendWithSwap` | Send with swap option | +| `ChooseToken` | Token chooser for send-via-swap | + +### CommonSendRoute (Send internal) +Used internally by `SendModel` and `NFTSendModel`: +- `Amount` → `Destination` → `Confirm` → `ConfirmSuccess` +- Edit mode: `Confirm` → `Destination(edit)` or `Amount(edit)` + +### FeeSelectorRoute (Send internal) +- `ChooseToken` — select fee token +- `ChooseSpeed` — select fee speed + +### WcInnerRoute (WalletConnect) +**File:** `features/walletconnect/impl/.../routing/WcInnerRoute.kt` + +| Route | Description | +|-------|-------------| +| `Method.Send` | WC send transaction | +| `Method.SignMessage` | WC sign message | +| `Method.AddNetwork` | WC add network | +| `Method.SwitchNetwork` | WC switch network | +| `Pair` | WC pairing request | +| `UnsupportedMethodAlert` | Unsupported method alert | +| `WcDappDisconnected` | DApp disconnected alert | +| `TangemUnsupportedNetwork` | Unsupported network alert | +| `RequiredAddNetwork` | Required network add | +| `RequiredReconnectWithNetwork` | Required network reconnect | + +### TangemPayDetailsInnerRoute +**File:** `features/tangempay/details/impl/.../navigation/TangemPayDetailsInnerRoute.kt` + +| Route | Description | +|-------|-------------| +| `Details` | Main details view | +| `ChangePIN` | Change PIN flow | +| `ChangePINSuccess` | PIN change success | +| `AddToWallet` | Add card to device wallet | + +Transitions: `Details` → `ChangePIN` → `ChangePINSuccess`, `Details` → `AddToWallet` + +### FeedEntryRoute +**File:** `features/feed/api/.../components/FeedEntryRoute.kt` + +| Route | Description | +|-------|-------------| +| `MarketTokenDetails` | Market token detail view | +| `MarketTokenList` | Markets list | +| `NewsDetail` | News article detail | + +### CreateWalletBackupRoute +**File:** `features/hot-wallet/impl/.../createwalletbackup/routing/CreateWalletBackupRoute.kt` + +| Route | Description | +|-------|-------------| +| `RecoveryPhraseStart` | Backup intro | +| `RecoveryPhrase` | Show recovery phrase | +| `ConfirmBackup` | Confirm backup | +| `BackupCompleted` | Backup complete (with upgrade/last screen flags) | + +Transitions: `RecoveryPhraseStart` → `RecoveryPhrase` → `ConfirmBackup` → `BackupCompleted` + +### AddExistingWalletRoute +**File:** `features/hot-wallet/impl/.../addexistingwallet/entry/routing/AddExistingWalletRoute.kt` + +| Route | Description | +|-------|-------------| +| `Import` | Seed phrase import | +| `BackupCompleted` | Backup completed | +| `SetAccessCode` | Set access code | +| `ConfirmAccessCode` | Confirm access code | +| `PushNotifications` | Push notification opt-in | +| `SetupFinished` | Setup complete | + +Transitions: `Import` → `BackupCompleted` → `SetAccessCode` → `ConfirmAccessCode` → `PushNotifications` → `SetupFinished` + +### UpdateAccessCodeRoute +**File:** `features/hot-wallet/impl/.../updateaccesscode/routing/UpdateAccessCodeRoute.kt` + +| Route | Description | +|-------|-------------| +| `SetAccessCode` | Enter new access code | +| `ConfirmAccessCode` | Confirm new access code | +| `SetupFinished` | Update complete | + +Transitions: `SetAccessCode` → `ConfirmAccessCode` → `SetupFinished` + +### WalletActivationRoute +**File:** `features/hot-wallet/impl/.../walletactivation/entry/routing/WalletActivationRoute.kt` + +| Route | Description | +|-------|-------------| +| `ManualBackupStart` | Backup intro | +| `ManualBackupPhrase` | Show recovery phrase | +| `ManualBackupCheck` | Verify backup | +| `ManualBackupCompleted` | Backup success | +| `SetAccessCode` | Set access code | +| `ConfirmAccessCode` | Confirm access code | +| `PushNotifications` | Push notification opt-in | +| `SetupFinished` | Activation complete | + +Transitions: `ManualBackupStart` → `ManualBackupPhrase` → `ManualBackupCheck` → `ManualBackupCompleted` → `SetAccessCode` → `ConfirmAccessCode` → `PushNotifications` → `SetupFinished` + +## 4. Deep Links + +### URI Schemes + +| Scheme | Value | Usage | +|--------|-------|-------| +| `Tangem` | `tangem://` | Primary app deep links | +| `WalletConnect` | `wc://` | WalletConnect pairing | +| `Https` | `https://` | Web links (tangem.com) | + +### Tangem Scheme Routes (`tangem://{host}`) + +| Host | Handler | Target | +|------|---------|--------| +| `onramp` | `OnrampDeepLinkHandler` | Onramp callback processing | +| `redirect_sell` | `SellRedirectDeepLinkHandler` | `Send` (sell redirect with tx params) | +| `redirect` | — | Buy redirect (no-op) | +| `buy` | `BuyDeepLinkHandler` | `BuyCrypto` | +| `sell` | `SellDeepLinkHandler` | `SellCrypto` | +| `swap` | `SwapDeepLinkHandler` | `SwapCrypto` | +| `referral` | `ReferralDeepLinkHandler` | Referral flow | +| `main` | `WalletDeepLinkHandler` | Wallet screen | +| `token` | `TokenDetailsDeepLinkHandler` | `CurrencyDetails` | +| `staking` | `StakingDeepLinkHandler` | `Staking` | +| `markets` | `MarketsDeepLinkHandler` | `Markets` | +| `token_chart` | `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` | +| `wc` | `WalletConnectDeepLinkHandler` | WalletConnect pairing | +| `promo` | `PromoDeeplinkHandler` | Promo handling | +| `onboard-visa` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` | + +### HTTPS Routes (`https://tangem.com/...`) + +| Path prefix | Handler | Target | +|-------------|---------|--------| +| `/pay-app` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` | +| `/news` | `NewsDetailsDeepLinkHandler` | `NewsDetails` | + +### Deep Link Readiness + +Deep links are only processed when the app is on a "ready" route. These routes **block** deep link processing: +- `Initial`, `Home`, `Welcome`, `PushNotification`, `Disclaimer`, `Stories`, `Onboarding` \ No newline at end of file diff --git a/.claude/rules/codestyle/drawable-naming.md b/.claude/rules/codestyle/drawable-naming.md new file mode 100644 index 0000000000..e18b07bd2e --- /dev/null +++ b/.claude/rules/codestyle/drawable-naming.md @@ -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. \ No newline at end of file diff --git a/.claude/rules/domain/core-components.md b/.claude/rules/domain/core-components.md new file mode 100644 index 0000000000..9b9584fd4e --- /dev/null +++ b/.claude/rules/domain/core-components.md @@ -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?>` for the wallets list and `StateFlow` 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`) 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. \ No newline at end of file diff --git a/.claude/rules/domain/core-models.md b/.claude/rules/domain/core-models.md new file mode 100644 index 0000000000..bd7bf66aba --- /dev/null +++ b/.claude/rules/domain/core-models.md @@ -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`): +- 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) +``` \ No newline at end of file diff --git a/.claude/rules/git-rules.md b/.claude/rules/git-rules.md new file mode 100644 index 0000000000..fbfba63f29 --- /dev/null +++ b/.claude/rules/git-rules.md @@ -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` \ No newline at end of file diff --git a/.claude/rules/tangem-sdk.md b/.claude/rules/tangem-sdk.md new file mode 100644 index 0000000000..daeb149428 --- /dev/null +++ b/.claude/rules/tangem-sdk.md @@ -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 \ No newline at end of file diff --git a/.claude/skills/analyze-logs/SKILL.md b/.claude/skills/analyze-logs/SKILL.md new file mode 100644 index 0000000000..4ecc6e0104 --- /dev/null +++ b/.claude/skills/analyze-logs/SKILL.md @@ -0,0 +1,215 @@ +--- +name: analyze-logs +description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation. +allowed-tools: Read, Grep +argument-hint: /path/to/logfile.txt [/path/to/logs.rtf] +--- + +Analyze the Tangem app user log file at path: `$ARGUMENTS` + +## File Input + +The user provides one or two file paths: +- **Log file** (`.txt`) — main application log, always required +- **User info file** (`.rtf` or `.txt`) — optional, contains card/device/error info from the user's feedback email + +If two paths are provided, the first is the log file and the second is the user info file. + +**If only the log file is provided**, ask the user if they have a user info file (`logs.rtf` or `logs.txt`). If they don't have it or don't respond, fill Device Context, Card Info, and Transaction Context sections from the log file data (Steps 2+3). Mark fields that could not be determined as "N/A". + +## User Info File (logs.rtf / logs.txt) + +If a user info file is provided, Read it and extract the plain text fields. The file contains structured key-value pairs like: + +``` +Card ID: AF36000002151580 +Firmware version: 6.33r +Linked cards count: 2 +Has seed phrase: true +Signed hashes [secp256k1]: 0 +---------- +Blockchain: Polygon +Explorer link: https://polygonscan.com/address/0x... +Derivation path: m/44'/60'/0'/0/0 +Host: https://rpc-mainnet.matic.quiknode.pro/ +Token: USDC +Error: Could not construct a recoverable key. +---------- +Source address: 0x... +Destination address: 0x... +Amount: 11.319684 +Fee: 0.004973 +---------- +Phone model: SM-S921B +OS version: 36 +App version: 5.34.1 +``` + +Extract all fields and include them in the **Device Context**, **Card Info**, and **Analysis Summary** sections of the report. If the RTF contains an `Error:` field, treat it as a key clue for the investigation. + +Note: RTF files contain formatting markup (`\cb3`, `\cf4`, `{\field{...}}`). Ignore all RTF tags — only extract the plain text values after each colon. + +## Log Format + +Each line follows the pattern: +``` +DD.MM HH:MM:SS.mmm: TAG Message +``` + +- Date format: `DD.MM` (day.month), no year — infer from context +- Multi-line entries (JSON bodies, stack traces) continue without the timestamp prefix +- Sensitive data is masked with `******` +- Continuation lines may start with `|` for structured data: `|- Duration millis: 300000` + +## Analysis Steps + +Use `head_limit` on every Grep call to protect context from overload. + +### Step 1: Setup + +1. **Log time range:** Read the first and last lines with dates (format `DD.MM`) +2. **Ask the user** (report time range, then ask): + - Date range to focus on (or "all" for the full file) + - Focus area: `Wallet`, `WalletConnect`, `Express (Onramp/Buy, Offramp/Sell, Swap/Exchange)`, `TangemPay`, `Feed`, `Markets`, `Settings`, `Referral`, `Staking`, `Onboarding`, `Send/Transactions`, `NFT`, or `all` + - Remember the chosen area as **FOCUS_AREA** +3. **Determine line range** (skip if "all"): + - Parse input into `DD.MM` patterns (`10-13.03` → start `10.03`, end `13.03`; `last day` → last date; `last 3 days` → 3 days before last) + - Find **START_LINE**: Grep `^START_DATE` (head_limit: 1, -n: true) + - Find **END_LINE**: Grep `^NEXT_DATE` (head_limit: 1, -n: true). If not found, END_LINE = end of file + - Report: "Focusing on lines START_LINE–END_LINE covering DD.MM–DD.MM" + +### Steps 2+3+4+5+6: Main Analysis (all in parallel) + +Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (device info may be before the date range). Steps 4+5+6 use `offset: START_LINE` to stay within the date range. + +**Device Context (full file):** +- `PATCH.*user-wallets/applications` (head_limit: 5, -A 10) — Read JSON body to extract `systemVersion`, `version`, `language`, `timezone` +- `ip_address` (head_limit: 5, -A 20) — extract `alpha2`, `country`, `isBuyAllowed`, `isSellAllowed` + +**Card Info (full file):** +- `CardSDK_Tlv.*TAG_Firmware` (head_limit: 20) +- `CardSDK_Tlv.*TAG_SettingsMask` (head_limit: 20) +- `CardSDK_Tlv.*TAG_IsActivated` (head_limit: 20) +- `CardSDK_Tlv.*TAG_ManufacturerName` (head_limit: 20) + +**Navigation (offset: START_LINE):** +- `AppRouter` (head_limit: 200) — if FOCUS_AREA is `all` or navigation-heavy (Wallet, Onboarding, Send/Transactions), also Read `.claude/docs/navigation-graph.md` to cross-reference routes + +**Errors (offset: START_LINE, head_limit: 50 each, -n: true):** +- HTTP errors: `<-- [45]\d{2}` +- Domain errors: `DomainError` +- App exceptions: `\bException\b` +- Biometric errors: `onAuthenticationError` +- Tangem Pay errors: `Failed checkCustomerWallet` + +**Session timeline (offset: START_LINE, head_limit: 50 each):** +- `MainActivity.*onCreate` — app session start +- `MainActivity.*Splash screen` — splash screen installed/dismissed +- `MainActivity.*onNewIntent` — deep link or push notification +- `CardSDK_Session.*start card session` — NFC session starts + +**Error filtering:** When processing error results, skip these noisy matches: +- `java.io.IOException: Canceled` — normal request cancellation +- `HttpException(code=304` — HTTP "Not Modified" +- Bare stacktrace lines starting with `\tat` +- `<-- HTTP FAILED: java.io.IOException: Canceled` + +### Step 7: Deep Dive + +For each significant error found above: +1. Note the error's line number from Grep output (`-n: true`) +2. Use Read with `offset: ERROR_LINE - 100, limit: 200` to get ~200 lines of context +3. In that context, look for navigation events, API calls, and redux actions + +## Key Tags Reference + +| Tag | Purpose | +|-----|---------| +| `MainActivity` | Activity lifecycle, splash screen, onNewIntent | +| `AppRouter` | Navigation: Push, Pop, Replace | +| `NetworkLogs` | HTTP requests/responses (OkHttp) | +| `BlockchainSDK_NETWORK` | Blockchain RPC calls | +| `CardSDK_Tlv` | NFC card data (firmware, settings) | +| `CardSDK_Session` | NFC session lifecycle | +| `CardSDK_Biometric` | Biometric authentication | + +## Common Error Patterns + +| Pattern | Meaning | +|---------|---------| +| `HttpException(code=4xx/5xx, errorBody={...})` | API error with structured body | +| `DomainError(description=...)` | App-level domain error | +| `<-- HTTP FAILED: java.io.IOException: Canceled` | Cancelled network request (noise) | +| `<-- 429` | Rate limiting | +| `onAuthenticationError` | Biometric auth failure | + +## Output Template + +Structure your report EXACTLY as follows: + +``` +# Log Analysis Report + +## Device Context +| Parameter | Value | +|-----------|-------| +| App version | ... | +| Android version | ... | +| Phone model | ... (from logs.rtf if available) | +| Language | ... | +| Timezone | ... | +| Country | ... | +| Log time range | DD.MM HH:MM — DD.MM HH:MM | + +## Card Info +| Parameter | Value | +|-----------|-------| +| Card ID | ... (from logs.rtf if available) | +| Firmware | ... | +| Manufacturer | ... | +| Is activated | ... | +| Linked cards | ... (from logs.rtf if available) | +| Has seed phrase | ... (from logs.rtf if available) | + +## Transaction Context (from logs.rtf, if available) +| Parameter | Value | +|-----------|-------| +| Blockchain | ... | +| Token | ... | +| Source address | ... | +| Destination address | ... | +| Amount | ... | +| Fee | ... | +| Error | ... | + +## Navigation Path +1. [HH:MM:SS] Screen (Push/Pop/Replace) +2. ... + +**Summary:** Brief description of the user's journey. + +## Errors Found + +### HTTP Errors +| Time | URL | Status | Details | +|------|-----|--------|---------| + +### Domain Errors +| Time | Component | Error | +|------|-----------|-------| + +### Other Errors +| Time | Type | Details | +|------|------|---------| + +## Key Events Timeline +| Time | Event | Details | +|------|-------|---------| +(chronological: app starts, card sessions, navigation, errors, notable API calls) + +## Analysis Summary +(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations. +If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.) +``` + +If a section has no data, write "None found" instead of omitting it. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..32252fee68 --- /dev/null +++ b/CLAUDE.md @@ -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()` +- 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` 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/` \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2c8978746..bb907bd129 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt index 0301a65b80..fbf86435f4 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -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}") } } diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt index f57b43288a..3e5c61cf6b 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -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) + } } } diff --git a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt index a8d1a2c038..098d2f4def 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt @@ -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}") } } } diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt index da1573d026..48c2a5cc5a 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt index 1095a7aa96..0f7a68f9b8 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt @@ -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) } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt index 8069b5d735..2cb635f684 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt @@ -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) + } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt index d7f75da532..37950eb1a3 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt @@ -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 { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt index 32177f7b76..6f0000086b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt @@ -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 diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt index 69307ea8b4..a9746ca3c1 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt @@ -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") { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt index 31b9035761..b9f6635d09 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt @@ -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) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt index 31cf64adce..1c481a80fb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt @@ -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 { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt index ce2450ee04..e1e79b96ad 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt @@ -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 diff --git a/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt b/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt index 627e41f2cc..39067a2ac9 100644 --- a/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt @@ -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) "" } } diff --git a/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt b/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt index 956e058583..cf4724d3f4 100644 --- a/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt +++ b/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt index 03c709cd6c..1136383fb5 100644 --- a/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt @@ -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}") "" } } diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt index b7a1de528d..cfd160dab2 100644 --- a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt @@ -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?) { diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt index 30db15d90c..b0e4f21128 100644 --- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index 62c9fa652d..40aace40cd 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 3124e26244..0798bcf8cf 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -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} diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index d6c1a7abf3..2d3a35f48c 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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() 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") } } } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 5dd5a03d4c..01efaeb7bf 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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 @@ -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, diff --git a/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt b/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt index 57f5a3fdec..627b631f1c 100644 --- a/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt +++ b/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index c863320c2a..7c984b6d60 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -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 { @@ -48,49 +39,12 @@ class DialogManager : StoreSubscriber { 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() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt index 29b584fa05..62505009a4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt @@ -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) { - Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) + TangemLogger.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) } override fun logException(error: Throwable, params: Map) { - Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params))) + TangemLogger.e(jsonConverter.prettyPrint(PrintEventModel(name, "error", params)), error) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt index ae2547a4a9..b43b358a5c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt @@ -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}") } } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt index a643b0142e..51352ef3ee 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -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), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt index a051eefeb9..a0391029a5 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt @@ -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?) { - 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?) { - 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()}") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt index 2f07e571b1..772c294d3b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt @@ -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) { - 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") } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt index 40dfd9b37a..611ca30dba 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt @@ -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) }, ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt index ddd534e938..098d41abf3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt index 62a36d09f1..6773cce899 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt index 5a8a85c072..a7117928b4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt index 1c33fe0909..478f23edeb 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt index 8013aa18f1..70d32a7166 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt new file mode 100644 index 0000000000..ce74ae1064 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 3aeeb7d9cd..06dd58d367 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -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 = try Result.Success(wallet) } } catch (exception: Exception) { - Timber.e(exception) + TangemLogger.e("Error", exception) val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager) if (!networkConnectionManager.isOnline) { diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index 39b02987d6..083711a2ca 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -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 ?: "", throwable) + } else if (message != null) { + logger.d(message) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt index 8eaafd7505..0231c5fb23 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -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+)+$") diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 9965c9a2f2..ba6b3ddcfe 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt deleted file mode 100644 index 8c9f98ebdd..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ /dev/null @@ -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 = 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() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index ae347d0807..89851458ac 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index a544190398..7dd3992c54 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -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 = { _, _ -> { nextDispatch -> { action -> - Timber.i("Dispatch action: ${action::class.java.simpleName}") + TangemLogger.i("Dispatch action: ${action::class.java.simpleName}") nextDispatch(action) } } diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt deleted file mode 100644 index 078691ca7c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt deleted file mode 100644 index e0eb7acf6f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index e27db731ed..74e7e95205 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt b/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt index 190af9d4eb..242483b1b1 100644 --- a/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt +++ b/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt @@ -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, ) diff --git a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt index 12e69c91b8..e0aea6d0e0 100644 --- a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt +++ b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt @@ -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") } } diff --git a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt index 10a4807522..308322f8aa 100644 --- a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt index 7bc5ae57c9..fdfcf8789e 100644 --- a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index 65e36a1536..0021307180 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 27f82f9b3b..f9b56d8d88 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListRepositoryModule.kt similarity index 99% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt rename to app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListRepositoryModule.kt index 1fab5009e0..d7d9089767 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListRepositoryModule.kt @@ -41,7 +41,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object UserWalletsListManagerModule { +internal object UserWalletsListRepositoryModule { @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt index f4a2c32083..1eb51a4e27 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index 2e96094cf1..ce85e49171 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -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 { - 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 { - 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 { 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 { 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) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 6ecd467e40..724134783f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 22e94ae7d3..ecce5aaf75 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -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() @@ -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}") } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt index 6e124933fc..10ec357e6b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index b8fd319359..7b156a7889 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt deleted file mode 100644 index ef465fad15..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt deleted file mode 100644 index e0e8bd6b12..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt +++ /dev/null @@ -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 = { 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, - ), - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt deleted file mode 100644 index db39ad8c56..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt deleted file mode 100644 index 9f5257bd04..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt deleted file mode 100644 index df8cc61880..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt deleted file mode 100644 index 6d5875be78..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 742e44ec5e..8807a899e6 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index ad66a52dce..a78fc262d8 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -7,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 { "" }}") { + 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( diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 301a49d9ba..fb71667669 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -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()) } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index bf888af247..fb1acbcd80 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -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 diff --git a/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt new file mode 100644 index 0000000000..264a9b3f0a --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt @@ -0,0 +1,245 @@ +package com.tangem.tap.routing + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.decompose.navigation.Router +import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ProxyAppRouterTest { + + private val innerRouter = mockk(relaxed = true) + private val snackbarHandler = mockk(relaxed = true) + private val analyticsExceptionHandler = mockk(relaxed = true) + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val config = mockk(relaxed = true) { + every { componentRouter } returns innerRouter + every { stack } returns listOf(AppRoute.Wallet) + every { snackbarHandler } returns this@ProxyAppRouterTest.snackbarHandler + every { initializedState } returns MutableStateFlow(true) + } + + @AfterEach + fun tearDown() { + clearMocks(innerRouter, snackbarHandler, analyticsExceptionHandler, config) + every { config.componentRouter } returns innerRouter + every { config.stack } returns listOf(AppRoute.Wallet) + every { config.snackbarHandler } returns snackbarHandler + every { config.initializedState } returns MutableStateFlow(true) + } + + private fun createRouter(routerScope: CoroutineScope): ProxyAppRouter { + every { config.routerScope } returns routerScope + return ProxyAppRouter(config, dispatchers, analyticsExceptionHandler) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Push { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.AppSettings + + // Act + router.push(route) + + // Assert + verify { innerRouter.push(route, any()) } + } + + @Test + fun `calls onComplete false when inner router throws`() = runTest { + // Arrange + val router = createRouter(this) + every { innerRouter.push(any(), any()) } throws RuntimeException("Navigation error") + var result: Boolean? = null + + // Act + router.push(AppRoute.AppSettings) { result = it } + + // Assert + assertThat(result).isFalse() + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceCurrent { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.AppSettings + + // Act + router.replaceCurrent(route) + + // Assert + verify { innerRouter.replaceCurrent(route, any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceAll { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.Wallet + + // Act + router.replaceAll(route) + + // Assert + verify { innerRouter.replaceAll(route, onComplete = any()) } + } + + @Test + fun `catches exception silently via safeNavigate`() = runTest { + // Arrange + val router = createRouter(this) + every { innerRouter.replaceAll(*anyVararg(), onComplete = any()) } throws RuntimeException("Error") + + // Act + val actual = runCatching { router.replaceAll(AppRoute.Wallet) }.isSuccess + + // Assert + assertThat(actual).isTrue() + verify { innerRouter.replaceAll(AppRoute.Wallet, onComplete = any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Pop { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + + // Act + router.pop() + + // Assert + verify { innerRouter.pop(any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class PopTo { + + @Test + fun `delegates to inner router with route`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.Wallet + + // Act + router.popTo(route) + + // Assert + verify { innerRouter.popTo(route, any()) } + } + + @Test + fun `delegates to inner router with routeClass`() = runTest { + // Arrange + val router = createRouter(this) + + // Act + router.popTo(AppRoute.Wallet::class) + + // Assert + verify { innerRouter.popTo(AppRoute.Wallet::class, any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class DefaultCompletionHandler { + + @Test + fun `does nothing on success`() { + // Arrange + val router = createRouter(mockk()) + + // Act + router.defaultCompletionHandler(isSuccess = true, errorMessage = "error") + + // Assert + verify(exactly = 0) { analyticsExceptionHandler.sendException(any()) } + verify(exactly = 0) { snackbarHandler.showSnackbar(text = any(), buttonTitle = any(), action = any()) } + } + + @Test + fun `sends analytics and shows snackbar on failure`() { + // Arrange + val router = createRouter(mockk()) + + // Act + router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed") + + // Assert + verify { analyticsExceptionHandler.sendException(any()) } + verify { snackbarHandler.showSnackbar(text = any(), buttonTitle = any(), action = any()) } + } + + @Test + fun `sends analytics without snackbar when handler is null`() { + // Arrange + every { config.snackbarHandler } returns null + val router = createRouter(mockk()) + + // Act + router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed") + + // Assert + verify { analyticsExceptionHandler.sendException(any()) } + verify(exactly = 0) { snackbarHandler.showSnackbar(text = any(), buttonTitle = any(), action = any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Stack { + + @Test + fun `returns config stack`() { + // Arrange + val router = createRouter(mockk()) + val expected = listOf(AppRoute.Wallet) + + // Act + val actual = router.stack + + // Assert + assertThat(actual).isEqualTo(expected) + } + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 1692f38f75..0cb5e4db8f 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -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) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 30cd9f7258..0e396106b3 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { implementation(deps.firebase.messaging) // end - implementation(deps.timber) implementation(deps.arrow.core) diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index b34c827c74..253b68bc9c 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -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) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt deleted file mode 100644 index 094ecd23eb..0000000000 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt b/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt index 9f3973c29b..49785fb80e 100644 --- a/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt +++ b/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt @@ -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 diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 6cc4b2bf7e..8c3453b855 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -8,23 +8,20 @@ import com.tangem.utils.converter.Converter class AccountIconItemStateConverter( val size: AccountIconSize = AccountIconSize.Default, -) : Converter { +) : Converter { - 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, + ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt index 150bc43608..c2403a7266 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt @@ -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 { +) : Converter { - 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), + ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index 4187ce21d2..c4a063c28b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -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) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index dbc955ab1e..9fdb77d943 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -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), ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt index fe59dbf317..990e4bc9c1 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -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 } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt new file mode 100644 index 0000000000..a681d0df23 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt @@ -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), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt index 41fdd9e856..dd99983320 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -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 -> { diff --git a/core/ab-tests/build.gradle.kts b/core/ab-tests/build.gradle.kts index d47ebe83f1..f557fcda9e 100644 --- a/core/ab-tests/build.gradle.kts +++ b/core/ab-tests/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other libraries */ - implementation(deps.timber) /** Core modules */ implementation(projects.core.analytics.models) diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt index 703c717a79..7869bdbc27 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt @@ -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) { - 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 { diff --git a/core/config-toggles/build.gradle.kts b/core/config-toggles/build.gradle.kts index 4b01f4402d..55ba623cd7 100644 --- a/core/config-toggles/build.gradle.kts +++ b/core/config-toggles/build.gradle.kts @@ -73,7 +73,6 @@ dependencies { /** Other libraries */ implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) /** Core modules */ diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt index f19eb0ce0b..f5d01447c9 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt @@ -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 (..). @@ -67,7 +67,7 @@ internal class Version private constructor(value: String) : Comparable return try { Version(value) } catch (exception: Exception) { - Timber.e(exception, "Invalid version - %s", value) + TangemLogger.e("Invalid version - $value", exception) return null } } diff --git a/core/datasource/CLAUDE.md b/core/datasource/CLAUDE.md new file mode 100644 index 0000000000..1d3332998b --- /dev/null +++ b/core/datasource/CLAUDE.md @@ -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`) + +### 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()` \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 353cb3f5ab..34a72ebb46 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -97,7 +97,6 @@ dependencies { implementation(deps.kotlin.datetime) /** Logging */ - implementation(deps.timber) /** Network */ implementation(deps.moshi) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index 28c7fa4f72..82329e9e3c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -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( private val wrappedCall: Call, @@ -41,10 +41,10 @@ internal class ApiResponseCallDelegate( 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) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index ec7f06e90d..e83dc434c0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -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 Response.toSafeApiResponse(analyticsErrorHandler: Anal ApiResponseError.HttpException(code, message(), errorBody) } } catch (e: Exception) { - Timber.e(e, "UnknownException occured") + TangemLogger.e("UnknownException occured", e) ApiResponseError.UnknownException(e) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b64ceb1ec2..4d63fd4b3a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -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 @GET("exchange-status") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index d08ebf45bc..4326d871bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -25,4 +25,7 @@ data class ExchangeQuoteResponse( @Json(name = "minAmount") val minAmount: BigDecimal, + @Json(name = "quoteId") + val quoteId: String? = null, + ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 739514fe49..6d851c0067 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -191,12 +191,12 @@ interface TangemTechApi { suspend fun getPromoBannerDisplays( @Query("walletId") walletId: String, @Query("placeholder") placeholder: String, - @Query("locale") locale: String, + @Query("lang") languageISOCode: String, ): ApiResponse - @PATCH("v1/displays/{displayId}") + @PATCH("v1/banner/displays/{displayId}") suspend fun dismissPromoBannerDisplay( - @Path("displayId") displayId: String, + @Path("displayId") displayId: Int, @Body body: DismissPromoBannerRequest, ): ApiResponse // endregion diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt index ca1a8e9e5e..bc69dbf9c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt @@ -5,20 +5,31 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PromoBannerDisplaysResponse( - @Json(name = "items") val items: List, + @Json(name = "items") + val items: List, ) @Suppress("BooleanPropertyNaming") @JsonClass(generateAdapter = true) data class PromoBannerDisplayDTO( - @Json(name = "id") val id: String, - @Json(name = "placeholder") val placeholder: String, - @Json(name = "priority") val priority: String, - @Json(name = "title") val title: String, - @Json(name = "subtitle") val subtitle: String, - @Json(name = "iconUrl") val iconUrl: String?, - @Json(name = "deeplink") val deeplink: String?, - @Json(name = "buttonEnabled") val buttonEnabled: Boolean, - @Json(name = "buttonText") val buttonText: String?, - @Json(name = "dismissable") val dismissable: Boolean, + @Json(name = "id") + val id: Int, + @Json(name = "placeholder") + val placeholder: String, + @Json(name = "priority") + val priority: String, + @Json(name = "title") + val title: String, + @Json(name = "subtitle") + val subtitle: String, + @Json(name = "iconUrl") + val iconUrl: String?, + @Json(name = "deeplink") + val deeplink: String?, + @Json(name = "buttonEnabled") + val buttonEnabled: Boolean, + @Json(name = "buttonText") + val buttonText: String?, + @Json(name = "dismissable") + val dismissable: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt index 9325df9a70..3a8e5629f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -9,8 +9,8 @@ import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.NetworkMoshi import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -49,7 +49,10 @@ class AssetLoader @Inject constructor( json = json, ) - Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + TangemLogger.e( + "Error", + IllegalStateException("Parsed config [$fileName] is null"), + ) } parsedConfig @@ -61,51 +64,65 @@ class AssetLoader @Inject constructor( json = json, ) - Timber.e(throwable, "Failed to load config [$fileName] from assets") + TangemLogger.e("Failed to load config [$fileName] from assets", throwable) null }, ) } /** Load list [V] values of asset file [fileName] */ - suspend inline fun loadList(fileName: String): List = runCatching(dispatchers.io) { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun loadList(fileName: String): List { + val result = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - val type = Types.newParameterizedType(List::class.java, V::class.java) - val adapter = moshi.adapter>(type) + val type = Types.newParameterizedType(List::class.java, V::class.java) + val adapter = moshi.adapter>(type) - adapter.fromJson(json) - } - .fold( + adapter.fromJson(json) + } + return result.fold( onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + if (parsedConfig == null) { + TangemLogger.e( + "Error", + IllegalStateException("Parsed config [$fileName] is null"), + ) + } parsedConfig.orEmpty() }, onFailure = { throwable -> - Timber.e(throwable, "Failed to load config [$fileName] from assets") + TangemLogger.e("Failed to load config [$fileName] from assets", throwable) emptyList() }, ) + } /** Load map [String] keys and [V] values of asset file [fileName] */ - suspend inline fun loadMap(fileName: String): Map = runCatching(dispatchers.io) { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun loadMap(fileName: String): Map { + val result = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) - val adapter = moshi.adapter>(type) + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) - adapter.fromJson(json) - } - .fold( + adapter.fromJson(json) + } + return result.fold( onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + if (parsedConfig == null) { + TangemLogger.e( + "Error", + IllegalStateException("Parsed config [$fileName] is null"), + ) + } parsedConfig.orEmpty() }, onFailure = { throwable -> - Timber.e(throwable, "Failed to load config [$fileName] from assets") + TangemLogger.e("Failed to load config [$fileName] from assets", throwable) emptyMap() }, ) + } fun sendException(fileName: String, isParsingSuccess: Boolean, json: String?) { analyticsExceptionHandler.sendException( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index de63c7bbcd..effeecade9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -9,7 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import dagger.Module @@ -47,13 +47,12 @@ class MoshiModule { .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), ) .add( - NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) - .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") - .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") - .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") - .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") - .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") - .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"), + NamePolymorphicAdapterFactory.of(PaymentAccountStatusValueDM::class.java) + .withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created") + .withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status") + .withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card") + .withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card") + .withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"), ) .add( PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt index 4da4c8b17d..6516a08182 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt @@ -1,10 +1,10 @@ package com.tangem.datasource.di +import com.tangem.utils.logging.TangemLogger import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody -import timber.log.Timber class StatusCodeInterceptor : Interceptor { @@ -12,7 +12,7 @@ class StatusCodeInterceptor : Interceptor { val originalResponse = chain.proceed(chain.request()) if (shouldInterceptResponse(originalResponse)) { - Timber.e("StatusCodeInterceptor INTERCEPTED%s", originalResponse.request.url.toString()) + TangemLogger.e("StatusCodeInterceptor INTERCEPTED${originalResponse.request.url}") val body = getBody().toResponseBody("application/json".toMediaTypeOrNull()) val code = getCode() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt index bc349dac1c..8226326f81 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt @@ -8,7 +8,7 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.wallets.models.AppsFlyerConversionData -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultAppsFlyerStore( private val appPreferencesStore: AppPreferencesStore, @@ -18,14 +18,14 @@ internal class DefaultAppsFlyerStore( val dto = appPreferencesStore.getObjectSyncOrNull(CONVERSION_DATA_KEY) ?: return null return ConversionDataConverter.convertBack(value = dto).also { - Timber.i("Getting conversion data from store: $it") + TangemLogger.i("Getting conversion data from store: $it") } } override suspend fun getUID(): String? = appPreferencesStore.getSyncOrNull(UID_KEY) override suspend fun store(value: AppsFlyerConversionData) { - Timber.i("Storing conversion data to store: $value") + TangemLogger.i("Storing conversion data to store: $value") val dto = ConversionDataConverter.convert(value) @@ -33,24 +33,24 @@ internal class DefaultAppsFlyerStore( } override suspend fun storeIfAbsent(value: AppsFlyerConversionData) { - Timber.i("Storing conversion data to store if absent: $value") + TangemLogger.i("Storing conversion data to store if absent: $value") appPreferencesStore.editData { preferences -> val saved = preferences[CONVERSION_DATA_KEY] if (saved == null) { - Timber.i("Conversion data is absent, storing $value") + TangemLogger.i("Conversion data is absent, storing $value") preferences.setObject(CONVERSION_DATA_KEY, ConversionDataConverter.convert(value)) } } } override suspend fun storeUIDIfAbsent(value: String) { - Timber.i("Storing UID to store if absent: $value") + TangemLogger.i("Storing UID to store if absent: $value") appPreferencesStore.editData { preferences -> val saved = preferences[UID_KEY] if (saved == null) { - Timber.i("UID is absent, storing $value") + TangemLogger.i("UID is absent, storing $value") preferences[UID_KEY] = value } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt deleted file mode 100644 index fab336d555..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.tangem.datasource.local.config.environment.converter - -import com.tangem.blockchain.common.* -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.utils.converter.Converter - -/** - * Converts [EnvironmentConfigModel] to [BlockchainSdkConfig] - * -[REDACTED_AUTHOR] - */ -internal object BlockchainSDKConfigConverter : Converter { - - override fun convert(value: EnvironmentConfigModel): BlockchainSdkConfig { - return BlockchainSdkConfig( - blockchairCredentials = BlockchairCredentials( - apiKey = value.blockchairApiKeys, - authToken = value.blockchairAuthorizationToken, - ), - blockcypherTokens = value.blockcypherTokens, - quickNodeSolanaCredentials = QuickNodeCredentials( - apiKey = value.quiknodeApiKey, - subdomain = value.quiknodeSubdomain, - ), - quickNodeBscCredentials = QuickNodeCredentials( - apiKey = value.bscQuiknodeApiKey, - subdomain = value.bscQuiknodeSubdomain, - ), - quickNodePlasmaCredentials = QuickNodeCredentials( - apiKey = value.quiknodePlasmaApiKey, - subdomain = value.quiknodePlasmaSubdomain, - ), - quickNodeMonadCredentials = QuickNodeCredentials( - apiKey = value.quiknodeMonadApiKey, - subdomain = value.quiknodeMonadSubdomain, - ), - infuraProjectId = value.infuraProjectId, - tronGridApiKey = value.tronGridApiKey, - nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey), - getBlockCredentials = createGetBlockCredentials(value), - kaspaSecondaryApiUrl = value.kaspaSecondaryApiUrl, - tonCenterCredentials = TonCenterCredentials( - mainnetApiKey = value.tonCenterKeys.mainnet, - testnetApiKey = value.tonCenterKeys.testnet, - ), - chiaFireAcademyApiKey = value.chiaFireAcademyApiKey, - chiaTangemApiKey = value.chiaTangemApiKey, - hederaArkhiaApiKey = value.hederaArkhiaKey, - polygonScanApiKey = value.polygonScanApiKey, - bittensorDwellirApiKey = value.bittensorDwellirApiKey, - bittensorOnfinalityApiKey = value.bittensorOnfinalityKey, - dwellirApiKey = value.dwellirApiKey, - koinosProApiKey = value.koinosProApiKey, - alephiumApiKey = value.alephiumTangemApiKey, - moralisApiKey = value.moralisApiKey, - etherscanApiKey = value.etherScanApiKey, - blinkApiKey = value.blinkApiKey, - tatumApiKey = value.tatumApiKey, - ) - } - - private fun createGetBlockCredentials(configValues: EnvironmentConfigModel): GetBlockCredentials? { - return configValues.getBlockAccessTokens?.let { accessTokens -> - GetBlockCredentials( - xrp = GetBlockAccessToken(jsonRpc = accessTokens.xrp?.jsonRPC), - cardano = GetBlockAccessToken(rosetta = accessTokens.cardano?.rosetta), - avalanche = GetBlockAccessToken(jsonRpc = accessTokens.avalanche?.jsonRPC), - eth = GetBlockAccessToken(jsonRpc = accessTokens.eth?.jsonRPC), - etc = GetBlockAccessToken(jsonRpc = accessTokens.etc?.jsonRPC), - fantom = GetBlockAccessToken(jsonRpc = accessTokens.fantom?.jsonRPC), - rsk = GetBlockAccessToken(jsonRpc = accessTokens.rsk?.jsonRPC), - bsc = GetBlockAccessToken(jsonRpc = accessTokens.bsc?.jsonRPC), - polygon = GetBlockAccessToken(jsonRpc = accessTokens.polygon?.jsonRPC), - gnosis = GetBlockAccessToken(jsonRpc = accessTokens.gnosis?.jsonRPC), - cronos = GetBlockAccessToken(jsonRpc = accessTokens.cronos?.jsonRPC), - solana = GetBlockAccessToken(jsonRpc = accessTokens.solana?.jsonRPC), - ton = GetBlockAccessToken(jsonRpc = accessTokens.ton?.jsonRPC), - tron = GetBlockAccessToken(rest = accessTokens.tron?.rest), - cosmos = GetBlockAccessToken(rest = accessTokens.cosmos?.rest), - near = GetBlockAccessToken(jsonRpc = accessTokens.near?.jsonRPC), - aptos = GetBlockAccessToken(rest = accessTokens.aptos?.rest), - dogecoin = GetBlockAccessToken( - jsonRpc = accessTokens.dogecoin?.jsonRPC, - blockBookRest = accessTokens.dogecoin?.blockBookRest, - ), - litecoin = GetBlockAccessToken( - jsonRpc = accessTokens.litecoin?.jsonRPC, - blockBookRest = accessTokens.litecoin?.blockBookRest, - ), - dash = GetBlockAccessToken( - jsonRpc = accessTokens.dash?.jsonRPC, - blockBookRest = accessTokens.dash?.blockBookRest, - ), - bitcoin = GetBlockAccessToken( - jsonRpc = accessTokens.bitcoin?.jsonRPC, - blockBookRest = accessTokens.bitcoin?.blockBookRest, - ), - algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), - zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC), - polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC), - base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC), - blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC), - filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC), - arbitrum = GetBlockAccessToken(jsonRpc = accessTokens.arbitrum?.jsonRPC), - bitcoinCash = GetBlockAccessToken( - jsonRpc = accessTokens.bitcoinCash?.jsonRPC, - blockBookRest = accessTokens.bitcoinCash?.blockBookRest, - ), - kusama = GetBlockAccessToken(jsonRpc = accessTokens.kusama?.jsonRPC), - moonbeam = GetBlockAccessToken(jsonRpc = accessTokens.moonbeam?.jsonRPC), - optimism = GetBlockAccessToken(jsonRpc = accessTokens.optimism?.jsonRPC), - polkadot = GetBlockAccessToken(jsonRpc = accessTokens.polkadot?.jsonRPC), - shibarium = GetBlockAccessToken(jsonRpc = accessTokens.shibarium?.jsonRPC), - sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC), - telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC), - tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest), - monad = GetBlockAccessToken(rest = accessTokens.monad?.rest), - stellar = GetBlockAccessToken(rest = accessTokens.stellar?.rest), - ) - } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt deleted file mode 100644 index 6d7733109a..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.datasource.local.config.environment.converter - -import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.utils.converter.Converter - -/** - * Converter from [EnvironmentConfigModel] to [EnvironmentConfig] - * -[REDACTED_AUTHOR] - */ -internal object EnvironmentConfigConverter : Converter { - - override fun convert(value: EnvironmentConfigModel): EnvironmentConfig { - return EnvironmentConfig( - moonPayApiKey = value.moonPayApiKey, - moonPayApiSecretKey = value.moonPayApiSecretKey, - mercuryoWidgetId = value.mercuryoWidgetId, - mercuryoSecret = value.mercuryoSecret, - blockchainSdkConfig = BlockchainSDKConfigConverter.convert(value = value), - amplitudeApiKey = value.amplitudeApiKey, - appsFlyerApiKey = value.appsFlyer.appsFlyerDevKey, - appsAppId = value.appsFlyer.appsFlyerAppID, - walletConnectProjectId = value.walletConnectProjectId, - express = value.express, - devExpress = value.devExpress, - stakeKitApiKey = value.stakeKitApiKey, - p2pApiKey = value.p2pApiKey, - blockAidApiKey = value.blockaidApiKey, - tangemApiKey = value.tangemApiKey, - tangemApiKeyDev = value.tangemApiKeyDev, - tangemApiKeyStage = value.tangemApiKeyStage, - yieldModuleApiKey = value.yieldModuleApiKey, - yieldModuleApiKeyDev = value.yieldModuleApiKeyDev, - bffStaticToken = value.bffStaticToken, - bffStaticTokenDev = value.bffStaticTokenDev, - gaslessTxApiKeyDev = value.gaslessTxApiKeyDev, - gaslessTxApiKey = value.gaslessTxApiKey, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt deleted file mode 100644 index 152c16e531..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.datasource.local.config.environment.models - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@Suppress("LongParameterList") -@JsonClass(generateAdapter = true) -class EnvironmentConfigModel( - @Json(name = "mercuryoWidgetId") val mercuryoWidgetId: String, - @Json(name = "mercuryoSecret") val mercuryoSecret: String, - @Json(name = "moonPayApiKey") val moonPayApiKey: String, - @Json(name = "moonPayApiSecretKey") val moonPayApiSecretKey: String, - @Json(name = "blockchairApiKeys") val blockchairApiKeys: List, - @Json(name = "blockchairAuthorizationToken") val blockchairAuthorizationToken: String?, - @Json(name = "quiknodeSubdomain") val quiknodeSubdomain: String, - @Json(name = "quiknodeApiKey") val quiknodeApiKey: String, - @Json(name = "bscQuiknodeSubdomain") val bscQuiknodeSubdomain: String, - @Json(name = "bscQuiknodeApiKey") val bscQuiknodeApiKey: String, - @Json(name = "quiknodePlasmaSubdomain") val quiknodePlasmaSubdomain: String, - @Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String, - @Json(name = "quiknodeMonadSubdomain") val quiknodeMonadSubdomain: String, - @Json(name = "quiknodeMonadApiKey") val quiknodeMonadApiKey: String, - @Json(name = "nowNodesApiKey") val nowNodesApiKey: String, - @Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?, - @Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys, - @Json(name = "blockcypherTokens") val blockcypherTokens: Set?, - @Json(name = "infuraProjectId") val infuraProjectId: String?, - @Json(name = "tronGridApiKey") val tronGridApiKey: String, - @Json(name = "amplitudeApiKey") val amplitudeApiKey: String, - @Json(name = "appsFlyer") val appsFlyer: AppsFlyerModel, - @Json(name = "kaspaSecondaryApiUrl") val kaspaSecondaryApiUrl: String, - @Json(name = "walletConnectProjectId") val walletConnectProjectId: String, - @Json(name = "chiaFireAcademyApiKey") val chiaFireAcademyApiKey: String?, - @Json(name = "chiaTangemApiKey") val chiaTangemApiKey: String?, - @Json(name = "devExpress") val devExpress: ExpressModel?, - @Json(name = "express") val express: ExpressModel?, - @Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?, - @Json(name = "polygonScanApiKey") val polygonScanApiKey: String?, - @Json(name = "stakeKitApiKey") val stakeKitApiKey: String?, - @Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?, - @Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?, - @Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?, - @Json(name = "dwellirApiKey") val dwellirApiKey: String?, - @Json(name = "koinosProApiKey") val koinosProApiKey: String?, - @Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?, - @Json(name = "moralisApiKey") val moralisApiKey: String?, - @Json(name = "nftScanApiKey") val nftScanApiKey: String?, - @Json(name = "blockaidApiKey") val blockaidApiKey: String?, - @Json(name = "tangemApiKey") val tangemApiKey: String?, - @Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?, - @Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?, - @Json(name = "etherscanApiKey") val etherScanApiKey: String?, - @Json(name = "yieldModuleApiKey") val yieldModuleApiKey: String?, - @Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?, - @Json(name = "blinkApiKey") val blinkApiKey: String?, - @Json(name = "tatumApiKey") val tatumApiKey: String?, - @Json(name = "bffStaticToken") val bffStaticToken: String?, - @Json(name = "bffStaticTokenDev") val bffStaticTokenDev: String?, - @Json(name = "gaslessTxApiKeyDev") val gaslessTxApiKeyDev: String?, - @Json(name = "gaslessTxApiKey") val gaslessTxApiKey: String?, -) - -@JsonClass(generateAdapter = true) -data class GetBlockAccessTokens( - @Json(name = "xrp") val xrp: GetBlockToken?, - @Json(name = "cardano") val cardano: GetBlockToken?, - @Json(name = "avalanche") val avalanche: GetBlockToken?, - @Json(name = "ethereum") val eth: GetBlockToken?, - @Json(name = "ethereumClassic") val etc: GetBlockToken?, - @Json(name = "fantom") val fantom: GetBlockToken?, - @Json(name = "rsk") val rsk: GetBlockToken?, - @Json(name = "bsc") val bsc: GetBlockToken?, - @Json(name = "polygon") val polygon: GetBlockToken?, - @Json(name = "xdai") val gnosis: GetBlockToken?, - @Json(name = "cronos") val cronos: GetBlockToken?, - @Json(name = "solana") val solana: GetBlockToken?, - @Json(name = "ton") val ton: GetBlockToken?, - @Json(name = "tron") val tron: GetBlockToken?, - @Json(name = "cosmos-hub") val cosmos: GetBlockToken?, - @Json(name = "near") val near: GetBlockToken?, - @Json(name = "aptos") val aptos: GetBlockToken?, - @Json(name = "dogecoin") val dogecoin: GetBlockToken?, - @Json(name = "litecoin") val litecoin: GetBlockToken?, - @Json(name = "dash") val dash: GetBlockToken?, - @Json(name = "bitcoin") val bitcoin: GetBlockToken?, - @Json(name = "algorand") val algorand: GetBlockToken?, - @Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?, - @Json(name = "zksync") val zksync: GetBlockToken?, - @Json(name = "base") val base: GetBlockToken?, - @Json(name = "blast") val blast: GetBlockToken?, - @Json(name = "filecoin") val filecoin: GetBlockToken?, - @Json(name = "arbitrum-one") val arbitrum: GetBlockToken?, - @Json(name = "bitcoinCash") val bitcoinCash: GetBlockToken?, - @Json(name = "kusama") val kusama: GetBlockToken?, - @Json(name = "moonbeam") val moonbeam: GetBlockToken?, - @Json(name = "optimism") val optimism: GetBlockToken?, - @Json(name = "polkadot") val polkadot: GetBlockToken?, - @Json(name = "shibarium") val shibarium: GetBlockToken?, - @Json(name = "sui") val sui: GetBlockToken?, - @Json(name = "telos") val telos: GetBlockToken?, - @Json(name = "tezos") val tezos: GetBlockToken?, - @Json(name = "monad") val monad: GetBlockToken?, - @Json(name = "stellar") val stellar: GetBlockToken?, -) - -@JsonClass(generateAdapter = true) -data class TonCenterKeys( - @Json(name = "mainnet") val mainnet: String, - @Json(name = "testnet") val testnet: String, -) - -@JsonClass(generateAdapter = true) -data class P2PKeys( - @Json(name = "mainnet") val mainnet: String, - @Json(name = "hoodi") val hoodi: String, -) - -@JsonClass(generateAdapter = true) -data class GetBlockToken( - @Json(name = "jsonRpc") val jsonRPC: String?, - @Json(name = "blockBookRest") val blockBookRest: String?, - @Json(name = "rest") val rest: String?, - @Json(name = "rosetta") val rosetta: String?, -) - -@JsonClass(generateAdapter = true) -data class ExpressModel( - @Json(name = "apiKey") - val apiKey: String, - @Json(name = "signVerifierPublicKey") - val signVerifierPublicKey: String, -) - -@JsonClass(generateAdapter = true) -data class AppsFlyerModel( - @Json(name = "appsFlyerDevKey") - val appsFlyerDevKey: String, - @Json(name = "appsFlyerAppID") - val appsFlyerAppID: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt new file mode 100644 index 0000000000..b3dcc73c77 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.local.config.environment.models + +data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String) + +data class P2PKeys(val mainnet: String, val hoodi: String) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index 007f386f0e..65dd45cf46 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -3,13 +3,14 @@ package com.tangem.datasource.local.logs import android.content.Context import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.* +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import org.joda.time.DateTime import org.joda.time.format.DateTimeFormatterBuilder -import timber.log.Timber import java.io.* import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -121,7 +122,7 @@ class AppLogsStore @Inject constructor( private fun createFileIfNotExist() { if (!logFile.exists()) { runCatching { logFile.createNewFile() } - .onFailure(Timber::e) + .onFailure { TangemLogger.e("Error", it) } } } @@ -129,7 +130,7 @@ class AppLogsStore @Inject constructor( scope.launch { mutex.withLock { runCatching { callback() } - .onFailure(Timber::e) + .onFailure { TangemLogger.e("Error", it) } } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt index 72835aa183..db9df71d9d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt @@ -17,7 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_ import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration import com.tangem.utils.coroutines.AppCoroutineScope -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Application preferences data store 'DataStore'. @@ -49,7 +49,7 @@ internal object PreferencesDataStore { private fun createCorruptionHandler(): ReplaceFileCorruptionHandler { return ReplaceFileCorruptionHandler( produceNewData = { corruptionException -> - Timber.w(corruptionException) + TangemLogger.w("Error", corruptionException) emptyPreferences() }, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index 589fb4d915..7b1ca88f44 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -11,43 +11,58 @@ import java.math.BigDecimal /** * Payment account status for storage in the local cache. * - * @see [com.tangem.domain.pay.PaymentAccountStatus] + * @see [com.tangem.domain.models.account.AccountStatus.Payment] */ @JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) -sealed interface PaymentAccountStatusDM { +sealed interface PaymentAccountStatusValueDM { @NameLabel("not_created") data class NotCreated( @Json(name = "not_created") val marker: Boolean = true, - ) : PaymentAccountStatusDM + ) : PaymentAccountStatusValueDM @NameLabel("kyc_status") data class UnderReview( @Json(name = "kyc_status") val kycStatus: KycStatus, - ) : PaymentAccountStatusDM + @Json(name = "customer_id") val customerId: String, + ) : PaymentAccountStatusValueDM @NameLabel("issuing_card") data class IssuingCard( @Json(name = "issuing_card") val marker: Boolean = true, - ) : PaymentAccountStatusDM + ) : PaymentAccountStatusValueDM - @NameLabel("locked") - data class Locked( - @Json(name = "locked") val marker: Boolean = true, - ) : PaymentAccountStatusDM - - @NameLabel("balance") - data class Loaded( + @NameLabel("active_card") + data class ActiveCard( + @Json(name = "active_card") val isLocked: Boolean, + @Json(name = "customer_id") val customerId: String, @Json(name = "card_id") val cardId: String, @Json(name = "last_four_digits") val lastFourDigits: String, - @Json(name = "balance") val balance: BigDecimal, @Json(name = "currency_code") val currencyCode: String, @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "is_pin_set") val isPinSet: Boolean, - ) : PaymentAccountStatusDM + @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, + @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, + ) : PaymentAccountStatusValueDM @NameLabel("card_issue_failed") data class CardIssueFailed( @Json(name = "card_issue_failed") val marker: Boolean = true, - ) : PaymentAccountStatusDM + @Json(name = "customer_id") val customerId: String, + ) : PaymentAccountStatusValueDM + + @JsonClass(generateAdapter = true) + data class FiatBalanceDM( + @Json(name = "available_balance") val availableBalance: BigDecimal, + @Json(name = "currency") val currency: String, + ) + + @JsonClass(generateAdapter = true) + data class CryptoBalanceDM( + @Json(name = "id") val id: String, + @Json(name = "chain_id") val chainId: Long, + @Json(name = "deposit_address") val depositAddress: String, + @Json(name = "token_contract_address") val tokenContractAddress: String, + @Json(name = "balance") val balance: BigDecimal, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt index b246a027cb..4451945383 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.utils +import com.tangem.utils.logging.TangemLogger import okhttp3.Interceptor import okhttp3.Response -import timber.log.Timber /** * OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance. @@ -17,7 +17,7 @@ class WireMockRedirectInterceptor : Interceptor { if (url.contains(WIREMOCK_REMOTE_URL)) { val newUrl = url.replace(WIREMOCK_REMOTE_URL, override.trimEnd('/')) - Timber.d("WireMockRedirect: $url -> $newUrl") + TangemLogger.d("WireMockRedirect: $url -> $newUrl") val newRequest = request.newBuilder() .url(newUrl) .build() diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt index 5650f72e16..1b1693d5a7 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -4,13 +4,13 @@ import com.google.common.truth.Truth import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend +import com.tangem.utils.logging.TangemLogger import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -38,7 +38,7 @@ class ApiConfigTest { // Actual val actual = allBaseUrls.all { it.endsWith("/") } - Timber.e(allBaseUrls.joinToString(separator = "\n")) + TangemLogger.e(allBaseUrls.joinToString(separator = "\n")) // Assert Truth.assertThat(actual).isTrue() diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index 8dc7d5437d..3020e44ee4 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -11,10 +11,11 @@ android { } dependencies { + implementation(projects.core.utils) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) implementation(deps.material) implementation(deps.reKotlin) - implementation(deps.timber) } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt b/core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt new file mode 100644 index 0000000000..daa8fee01b --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt @@ -0,0 +1,11 @@ +package com.tangem.core.navigation.deeplink + +/** + * Routes in-app content links to the appropriate destination. + * - Deep link schemes (e.g. `tangem://`, `wc://`) → handled in-app + * - Web URLs (`https://`) → opened in browser + */ +interface DeeplinkLauncher { + + fun launch(link: String) +} \ No newline at end of file diff --git a/core/res/build.gradle.kts b/core/res/build.gradle.kts index c444a12b01..9fe72ddbb5 100644 --- a/core/res/build.gradle.kts +++ b/core/res/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { implementation(projects.core.utils) - implementation(deps.timber) // region Firebase libraries implementation(platform(deps.firebase.bom)) diff --git a/core/res/src/main/java/com/tangem/core/res/Resources.kt b/core/res/src/main/java/com/tangem/core/res/Resources.kt index 3964c5c093..d795fd2f00 100644 --- a/core/res/src/main/java/com/tangem/core/res/Resources.kt +++ b/core/res/src/main/java/com/tangem/core/res/Resources.kt @@ -5,7 +5,7 @@ import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.utils.SupportedLanguages -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Get a string resource safely or the resource name if an exception is thrown @@ -87,7 +87,7 @@ private fun reportIssue(throwable: Throwable, resources: Resources, id: Int, var "\terror message: ${throwable.message.orEmpty()}\n", ) - Timber.tag("Resources").e(exception) + TangemLogger.withTag("Resources").e("Error", exception) FirebaseCrashlytics.getInstance().recordException(exception) } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index c4760bb8a5..72e7efe111 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -404,6 +404,7 @@ Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. Wert kopiert Meine Wallet + Warnung Woche mit Ja diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 4fde82b922..1b40ed9ebf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -401,6 +401,7 @@ Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. Valor copiado Billeteras + Alerta semana con diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 89c9435c9b..772f4d92d2 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -400,6 +400,7 @@ Unstakez En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant. Valeur copiée + Alerte semaine avec Oui diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 9cd39c1c7a..0282ef7661 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -26,6 +26,7 @@ %d gettone %d gettoni + Avviso Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 6efdf8cd26..e4a52817fd 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -301,6 +301,7 @@ 時間 インポート 進行中 + 残高不足 後で もっと詳しく 残り%1$s @@ -397,6 +398,7 @@ %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 値がコピーされました ウォレット + 警告 はい @@ -473,7 +475,7 @@ 絞り込み マイネットワーク ネットワーク - よく使われています + 人気 該当する結果はありません ステーキング・利息モード こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 04ca28b1d4..de83ee0e8f 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -404,6 +404,7 @@ Devido a %1$s limitações apenas %2$d Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade. Valor copiado Carteiras + Aviso semana com Sim diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index a624ed8ccd..2654c8ae24 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -415,6 +415,7 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Значение скопировано Кошельки + Предупреждение неделю с Да diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 14557017ad..67cdf8af99 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -415,6 +415,7 @@ Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. Скопійовано Гаманці + Увага тиждень з Так diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c410e833bc..c18d1cec74 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -99,6 +99,7 @@ 我了解 無法觸達 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 + 警告 已複製代幣地址 支持的網路 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5f645487de..02c1920d57 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -405,6 +405,7 @@ Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Value copied Wallets + Warning week with Yes @@ -481,7 +482,7 @@ Filter by My networks Networks - Mostly used + Popular No results Staking & Yield mode Hi support team, I\'ve encountered an error with code: %s @@ -703,7 +704,7 @@ Not enough Mana You can transfer only %s due to the Mana limit imposed by the Koinos network Mana limit - The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana + The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana Mana level To begin tracking your crypto assets and transactions, add tokens Manage tokens @@ -829,6 +830,7 @@ Position in crypto rating between all coins based on market capitalization Market position Max supply + Circulating and Max Supply The maximum number of coins or tokens that can ever exist for a particular cryptocurrency Max supply Metrics @@ -1666,6 +1668,7 @@ Token in %%image%% %1$s network The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s + N/A Show QR code Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 9be1ed52f9..87b86b8135 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -59,7 +59,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) api(deps.jodatime) - implementation(deps.timber) implementation(deps.markdown) api(deps.haze) { exclude(module = "activity-compose") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 7dd6ee0767..3349f73c68 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_S import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode import com.tangem.core.ui.utils.defaultFormat import com.tangem.core.ui.utils.formatWithThousands -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -78,7 +78,7 @@ class AmountVisualTransformation( currency = formatterCurrency } val formatter = requireNotNull(numberFormatter as? DecimalFormat) { - Timber.e("NumberFormat is null") + TangemLogger.e("NumberFormat is null") return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN) } return buildAnnotatedString { diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index 9b687679f5..c05ffb7daa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -102,6 +102,7 @@ fun TangemTopBar( endContent = endContent, content = { Column( + modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), ) { @@ -128,47 +129,54 @@ fun TangemTopBar( * A top bar composable that displays a title and optional start and end icons. * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev) * - * @param modifier Modifier to be applied to the top bar. - * @param type Type of the top bar, which determines its size and padding. - * @param content Composable content to be displayed in the center of the top bar - * @param startContent Optional composable content to be displayed at the start (left) of the top bar. - * @param endContent Optional composable content to be displayed at the end (right) of the top bar. + * @param modifier Modifier to be applied to the top bar. + * @param type Type of the top bar, which determines its size and padding. + * @param content Composable content to be displayed in the center of the top bar + * @param startContent Optional composable content to be displayed at the start (left) of the top bar. + * @param endContent Optional composable content to be displayed at the end (right) of the top bar. + * @param reserveSlotSpace If true (default), slot space is always reserved even when start/end content is null, + * ensuring centered alignment of [content]. Set to false for edge-to-edge content like + * search fields, where empty slots should not consume space. */ @Composable fun TangemTopBar( modifier: Modifier = Modifier, type: TangemTopBarType = TangemTopBarType.Default, - content: @Composable () -> Unit, + reserveSlotSpace: Boolean = true, + content: @Composable RowScope.() -> Unit, startContent: @Composable (() -> Unit)? = null, endContent: @Composable (() -> Unit)? = null, ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier .fillMaxWidth() .heightIn(min = type.getSize()) .padding(type.getPadding()), ) { - AnimatedContent( - targetState = startContent != null, - modifier = Modifier.size(TangemTheme.dimens2.x11), - label = "Start Content Visibility", - ) { isVisible -> - if (isVisible) { - startContent?.invoke() + if (reserveSlotSpace || startContent != null) { + AnimatedContent( + targetState = startContent != null, + modifier = Modifier.size(TangemTheme.dimens2.x11), + label = "Start Content Visibility", + ) { isVisible -> + if (isVisible) { + startContent?.invoke() + } } } content() - AnimatedContent( - targetState = endContent != null, - modifier = Modifier.size(TangemTheme.dimens2.x11), - label = "End Content Visibility", - ) { isVisible -> - if (isVisible) { - endContent?.invoke() + if (reserveSlotSpace || endContent != null) { + AnimatedContent( + targetState = endContent != null, + modifier = Modifier.size(TangemTheme.dimens2.x11), + label = "End Content Visibility", + ) { isVisible -> + if (isVisible) { + endContent?.invoke() + } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index dec4c24085..ff431ff036 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -13,6 +13,22 @@ import com.tangem.core.ui.message.EventMessageAction */ object Dialogs { + /** + * NFC feature is unavailable dialog + */ + fun nfcFeatureUnavailable(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.common_error), + message = resourceReference(R.string.nfc_error_unavailable), + ) + + /** + * Wrong wallet tapped dialog + */ + fun wrongWalletTapped(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.common_warning), + message = resourceReference(id = R.string.error_wrong_wallet_tapped), + ) + /** * Card verification failed dialog * diff --git a/core/ui/src/main/res/drawable/ic_fixed.xml b/core/ui/src/main/res/drawable/ic_fixed.xml new file mode 100644 index 0000000000..585d3632c3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fixed.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_fixed_32.xml b/core/ui/src/main/res/drawable/ic_fixed_32.xml new file mode 100644 index 0000000000..3197cc1649 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fixed_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_floating.xml b/core/ui/src/main/res/drawable/ic_floating.xml new file mode 100644 index 0000000000..0b94fd88fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_floating.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_floating_32.xml b/core/ui/src/main/res/drawable/ic_floating_32.xml new file mode 100644 index 0000000000..c2ccfb5a40 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_floating_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp new file mode 100644 index 0000000000..5768fd6820 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp differ diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index 48e632eff4..0d59823349 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -23,6 +23,10 @@ dependencies { implementation(deps.jodatime) // endregion + // region Logging + implementation(deps.kermit) + // endregion + testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt index 9b92b1cf18..582d6d4b57 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt @@ -1,10 +1,9 @@ package com.tangem.utils.coroutines +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineExceptionHandler import java.io.PrintWriter import java.io.StringWriter -import java.util.logging.Level -import java.util.logging.Logger /** [REDACTED_AUTHOR] @@ -16,11 +15,7 @@ object FeatureCoroutineExceptionHandler { val sw = StringWriter() throwable.printStackTrace(PrintWriter(sw)) val exceptionAsString: String = sw.toString() - // it delegates logging to android Logger, cause cant use timber in java module - Logger.getLogger("CoroutineExceptHandler").log( - Level.INFO, - "CoroutineException: from: $from, exception: $exceptionAsString", - ) + TangemLogger.i("CoroutineException: from: $from, exception: $exceptionAsString") throw throwable } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt b/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt new file mode 100644 index 0000000000..5bd1fab06f --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt @@ -0,0 +1,63 @@ +package com.tangem.utils.logging + +import co.touchlab.kermit.Logger + +/** + * Application-level logger that wraps Kermit [Logger] with the same API. + * All modules should use [TangemLogger] instead of importing Kermit directly. + */ +object TangemLogger { + + fun v(messageString: String, throwable: Throwable? = null) { + Logger.v(messageString, throwable) + } + + fun d(messageString: String, throwable: Throwable? = null) { + Logger.d(messageString, throwable) + } + + fun i(messageString: String, throwable: Throwable? = null) { + Logger.i(messageString, throwable) + } + + fun w(messageString: String, throwable: Throwable? = null) { + Logger.w(messageString, throwable) + } + + fun e(messageString: String, throwable: Throwable? = null) { + Logger.e(messageString, throwable) + } + + fun a(messageString: String, throwable: Throwable? = null) { + Logger.a(messageString, throwable) + } + + fun withTag(tag: String): TaggedLogger = TaggedLogger(tag) + + class TaggedLogger internal constructor(private val tag: String) { + + fun v(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).v(messageString, throwable) + } + + fun d(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).d(messageString, throwable) + } + + fun i(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).i(messageString, throwable) + } + + fun w(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).w(messageString, throwable) + } + + fun e(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).e(messageString, throwable) + } + + fun a(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).a(messageString, throwable) + } + } +} \ No newline at end of file diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index a07d7fac8a..9f3fa0d8a4 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -33,8 +33,11 @@ dependencies { api(projects.domain.models) api(projects.domain.tokens) api(projects.domain.wallets) + api(projects.domain.visa) // endregion + implementation(projects.features.tangempay.details.api) // Remove after TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + // region Project - Data implementation(projects.data.common) // endregion @@ -47,6 +50,7 @@ dependencies { // region Tangem dependencies implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) // endregion // region DI @@ -63,7 +67,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) // endregion // region Test diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt index b655bbbdcc..adb70a2b7e 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt @@ -7,9 +7,9 @@ import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -30,7 +30,7 @@ internal class DefaultMultiAccountListFetcher @Inject constructor( when (params) { is MultiAccountListFetcher.Params.Set -> { if (params.ids.isEmpty()) { - Timber.d("No wallet ids provided to fetch accounts.") + TangemLogger.d("No wallet ids provided to fetch accounts.") return@either } @@ -52,7 +52,7 @@ internal class DefaultMultiAccountListFetcher @Inject constructor( "Failed to fetch accounts for wallets:\n${errors.entries.joinToString(separator = "\n")}", ) - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 0e0b010f72..361622055b 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -16,7 +16,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.domain.models.wallet.UserWalletId -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -60,7 +60,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( ): FetchResult { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) if (isResponseUpToDate) { - Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") + TangemLogger.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") val response = requireNotNull(savedAccountsResponse) { "Saved accounts response is null for wallet: $userWalletId" } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 8eb7e90e0d..28fd5fd6be 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -1,10 +1,18 @@ package com.tangem.data.account.producer import arrow.core.Option +import arrow.core.getOrElse import arrow.core.none +import com.tangem.common.card.FirmwareVersion import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.features.tangempay.TangemPayFeatureToggles +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -12,6 +20,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map /** * Default implementation of [SingleAccountListProducer]. @@ -27,6 +36,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @Assisted val params: SingleAccountListProducer.Params, override val flowProducerTools: FlowProducerTools, private val walletAccountListFlowFactory: WalletAccountListFlowFactory, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountListProducer { @@ -34,8 +45,32 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { - return walletAccountListFlowFactory.create(userWalletId = params.userWalletId) - .flowOn(dispatchers.default) + val accountListFlow: Flow = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) { + combineWithPaymentAccount() + } else { + walletAccountListFlowFactory.create(userWalletId = params.userWalletId) + } + + return accountListFlow.flowOn(dispatchers.default) + } + + private fun combineWithPaymentAccount(): Flow { + return walletAccountListFlowFactory.create(params.userWalletId) + .map { accountList -> + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) + if (userWallet.isPaymentAccountSupported()) { + accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable -> + error("Can not combine account list and payment account status: $throwable") + } + } else { + accountList + } + } + } + + private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) { + is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } @AssistedFactory diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index ef589cc97d..65e2f11bde 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -32,10 +32,10 @@ import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.replaceBy +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -174,7 +174,7 @@ internal class DefaultAccountsCRUDRepository( val response = getAccountsResponseSync(userWalletId = userWalletId) if (response == null) { - Timber.e("Can't sync tokens. No accounts response found for wallet: $userWalletId") + TangemLogger.e("Can't sync tokens. No accounts response found for wallet: $userWalletId") return } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt index b48fff6967..36343bcd89 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt @@ -20,12 +20,12 @@ import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.local.accounts.AccountTokenMigrationStore import com.tangem.datasource.utils.getSyncOrNull import com.tangem.domain.account.tokens.MainAccountTokensMigration -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.derivation.AccountNodeRecognizer +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.launch -import timber.log.Timber /** * Implementation of [MainAccountTokensMigration] for migrating tokens associated with a main account. @@ -49,7 +49,7 @@ internal class DefaultMainAccountTokensMigration( val response = store.getSyncOrNull() ensureNotNull(response) { val exception = IllegalStateException("No cached accounts response found") - Timber.e(exception) + TangemLogger.e("Error", exception) exception } val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main) @@ -57,7 +57,7 @@ internal class DefaultMainAccountTokensMigration( .filterNot { accountDTO -> accountDTO.derivationIndex.toDerivationIndex().isMain } if (customAccounts.isEmpty()) { - Timber.i("There is only the Main account. Nothing to migrate") + TangemLogger.i("There is only the Main account. Nothing to migrate") return@either response } @@ -66,7 +66,7 @@ internal class DefaultMainAccountTokensMigration( .filter { it.key.value in customAccountIndexes } if (unassignedTokens.isEmpty()) { - Timber.i("No unassigned tokens found for migration") + TangemLogger.i("No unassigned tokens found for migration") return@either response } @@ -103,7 +103,7 @@ internal class DefaultMainAccountTokensMigration( derivationIndex: DerivationIndex, ): Either = either { if (derivationIndex == DerivationIndex.Main) { - Timber.i("Migration skipped: derivation index is Main") + TangemLogger.i("Migration skipped: derivation index is Main") return@either } @@ -113,7 +113,7 @@ internal class DefaultMainAccountTokensMigration( ensureNotNull(response) { val exception = IllegalStateException("No cached accounts response found") - Timber.e(exception) + TangemLogger.e("Error", exception) exception } @@ -123,7 +123,7 @@ internal class DefaultMainAccountTokensMigration( val unassignedTokens = mainAccount.findUnassignedTokens(derivationIndex) if (unassignedTokens.isNullOrEmpty()) { - Timber.i("No unassigned tokens found for migration") + TangemLogger.i("No unassigned tokens found for migration") return@either } @@ -158,7 +158,7 @@ internal class DefaultMainAccountTokensMigration( return ensureNotNull(account) { val exception = IllegalStateException("No account found with derivation index: $derivationIndex") - Timber.e(exception) + TangemLogger.e("Error", exception) exception } } @@ -198,13 +198,13 @@ internal class DefaultMainAccountTokensMigration( return filter { savedToken -> val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId) if (blockchain == null) { - Timber.e("Token has unknown networkId: $savedToken") + TangemLogger.e("Token has unknown networkId: $savedToken") return@filter false } val derivationPathValue = savedToken.derivationPath if (derivationPathValue == null) { - Timber.e("Token has no derivation path: $savedToken") + TangemLogger.e("Token has no derivation path: $savedToken") return@filter false } @@ -212,7 +212,7 @@ internal class DefaultMainAccountTokensMigration( val accountNodeValue = accountNodeRecognizer.recognize(derivationPathValue) if (accountNodeValue == null) { - Timber.e("Token has unrecognized derivation path: $savedToken") + TangemLogger.e("Token has unrecognized derivation path: $savedToken") return@filter false } @@ -230,7 +230,7 @@ internal class DefaultMainAccountTokensMigration( eTagsStore.clear(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts) } val exception = IllegalStateException("Failed to push updated tokens after migration") - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) }, ) diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt index b1069afb7c..4837e4f2a6 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt @@ -3,10 +3,12 @@ package com.tangem.data.account.producer import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -29,6 +31,10 @@ class DefaultSingleAccountListProducerTest { private val userWalletId = UserWalletId("011") private val flowProducerTools: FlowProducerTools = mockk() + private val tangemPayFeatureToggles = mockk { + every { this@mockk.isTangemPayAccountsRefactorEnabled } returns false + } + private val userWalletsListRepository = mockk() private val userWallet = mockk { every { this@mockk.walletId } returns userWalletId } @@ -38,6 +44,8 @@ class DefaultSingleAccountListProducerTest { walletAccountListFlowFactory = walletAccountListFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), flowProducerTools = flowProducerTools, + tangemPayFeatureToggles = tangemPayFeatureToggles, + userWalletsListRepository = userWalletsListRepository, ) @AfterEach diff --git a/data/analytics/build.gradle.kts b/data/analytics/build.gradle.kts index 279caa054f..1ad08811c1 100644 --- a/data/analytics/build.gradle.kts +++ b/data/analytics/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** Project - Analytics */ implementation(projects.core.analytics.models) + implementation(projects.core.utils) /** Project - Data */ implementation(projects.core.datasource) @@ -32,5 +33,4 @@ dependencies { /** Other */ implementation(deps.kotlin.coroutines) - implementation(deps.timber) } \ No newline at end of file diff --git a/data/app-currency/build.gradle.kts b/data/app-currency/build.gradle.kts index ca214f10a9..719c23cae7 100644 --- a/data/app-currency/build.gradle.kts +++ b/data/app-currency/build.gradle.kts @@ -33,5 +33,4 @@ dependencies { /** Other */ implementation(deps.jodatime) implementation(deps.kotlin.coroutines) - implementation(deps.timber) } \ No newline at end of file diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt index 292a209dc4..7afc9d5717 100644 --- a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt @@ -25,10 +25,10 @@ internal class FlipListener(private val action: () -> Unit) : SensorEventListene isScreenDown = true lastTriggerTime = currentTime // TODO add module logging - // Timber.tag("onSensorChanged").d("screen down") + // Logger.withTag("onSensorChanged").d("screen down") } else if (zAxisValue >= zAxisThreshold) { if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) { - // Timber.tag("onSensorChanged").d("screen up!") + // Logger.withTag("onSensorChanged").d("screen up!") lastTriggerTime = currentTime action.invoke() } diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index cb2baa1194..e8cda44907 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.kotlin.coroutines) - implementation(deps.timber) /* Test */ testImplementation(projects.common.test) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt index 4cdf5d1178..b3b0576829 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt @@ -4,8 +4,8 @@ import arrow.core.raise.Raise import arrow.core.raise.recover import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withTimeoutOrNull -import timber.log.Timber import kotlin.time.Duration /** @@ -67,7 +67,7 @@ suspend inline fun safeApiCall( ): T = recover( block = { call(ApiResponseRaise(raise = this)) }, recover = { error -> - Timber.e(error, "Unable to perform safe API call") + TangemLogger.e("Unable to perform safe API call", error) onError(error) }, ) \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 3ad4dfe46c..ecd52fa3ac 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -2,13 +2,13 @@ package com.tangem.data.common.cache import com.tangem.datasource.local.cache.CacheKeysStore import com.tangem.datasource.local.cache.model.CacheKey +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.joda.time.Duration import org.joda.time.LocalDateTime -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap internal class DefaultCacheRegistry( @@ -26,17 +26,17 @@ internal class DefaultCacheRegistry( } override suspend fun invalidate(key: String) { - Timber.d("Invalidate the cache key: $key") + TangemLogger.d("Invalidate the cache key: $key") withContext(NonCancellable) { cacheKeysStore.remove(key) } } override suspend fun invalidate(keys: Collection) { - Timber.d("Invalidate cache keys: $keys") + TangemLogger.d("Invalidate cache keys: $keys") withContext(NonCancellable) { cacheKeysStore.remove(keys) } } override suspend fun invalidateAll() { - Timber.d("Invalidate all cache keys") + TangemLogger.d("Invalidate all cache keys") withContext(NonCancellable) { cacheKeysStore.clear() } } @@ -58,7 +58,7 @@ internal class DefaultCacheRegistry( } try { - Timber.d("Invoke the action associated with the cache key: $key") + TangemLogger.d("Invoke the action associated with the cache key: $key") cacheKeysStore.store( key = CacheKey( @@ -70,7 +70,7 @@ internal class DefaultCacheRegistry( block() } catch (e: Throwable) { - Timber.e(e, "The action related to the cache key has failed: $key") + TangemLogger.e("The action related to the cache key has failed: $key", e) invalidate(key) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt index 3fe201e5de..59a7d62ae3 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.models.wallet.UserWalletId -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Default implementation of the [ETagsStore] interface for managing ETag values @@ -25,7 +25,7 @@ internal class DefaultETagsStore( override suspend fun store(userWalletId: UserWalletId, key: ETagsStore.Key, value: String) { if (value.isBlank()) { - Timber.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key") + TangemLogger.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key") return } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 14bb34f708..4b7cffe26c 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.Token as SdkToken // FIXME: Make internal @@ -52,7 +52,7 @@ class CryptoCurrencyFactory( accountIndex: DerivationIndex? = null, ): CryptoCurrency.Token? { if (blockchain == Blockchain.Unknown) { - Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + TangemLogger.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } @@ -93,7 +93,7 @@ class CryptoCurrencyFactory( accountIndex = accountIndex, ) } else { - Timber.e("Unable to get blockchain from chainId == $chainId") + TangemLogger.e("Unable to get blockchain from chainId == $chainId") null } } @@ -105,7 +105,7 @@ class CryptoCurrencyFactory( accountIndex: DerivationIndex? = null, ): CryptoCurrency.Coin? { if (blockchain == Blockchain.Unknown) { - Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + TangemLogger.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 06bc2f1e85..a939336175 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import com.tangem.blockchain.common.Token as SdkToken @@ -46,7 +46,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( ): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { - Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") + TangemLogger.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } @@ -71,7 +71,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( ): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { - Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") + TangemLogger.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt index 3594f6f1a0..6a21673603 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.derivation.AccountNodeRecognizer -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Enriches the [UserTokensResponse] with accountId values for tokens @@ -61,20 +61,20 @@ object UserTokensResponseAccountIdEnricher { .groupBy { savedToken -> val derivationPathValue = savedToken.derivationPath if (derivationPathValue == null) { - Timber.e("Token $savedToken has no derivation path") + TangemLogger.e("Token $savedToken has no derivation path") return@groupBy null } val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId) if (blockchain == null) { - Timber.e("Token $savedToken has unknown networkId") + TangemLogger.e("Token $savedToken has unknown networkId") return@groupBy null } val accountNodeRecognizer = AccountNodeRecognizer(blockchain) val accountIndex = accountNodeRecognizer.recognize(derivationPathValue) if (accountIndex == null) { - Timber.e("Token $savedToken has unrecognized derivation path") + TangemLogger.e("Token $savedToken has unrecognized derivation path") return@groupBy null } @@ -89,7 +89,7 @@ object UserTokensResponseAccountIdEnricher { if (accountIndex == null) return@mapKeys null val derivationIndex = DerivationIndex.invoke(value = accountIndex.toInt()).getOrElse { - Timber.e("Failed to parse derivation index from account index: $accountIndex") + TangemLogger.e("Failed to parse derivation index from account index: $accountIndex") return@mapKeys null } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 5796a28601..a745dfe274 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -12,10 +12,10 @@ import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.retryer.Retryer import com.tangem.utils.retryer.RetryerPool import kotlinx.coroutines.withContext -import timber.log.Timber @Suppress("LongParameterList") class UserTokensSaver( @@ -36,7 +36,7 @@ class UserTokensSaver( val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) if (userWallet == null) { - Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") + TangemLogger.e("UserWallet with id $userWalletId not found. Cannot push tokens.") onFailSend() return@withContext } @@ -116,7 +116,7 @@ class UserTokensSaver( userWalletId = userWalletId, response = response, onFailSend = { - Timber.e( + TangemLogger.e( "Retryer: Failed to push updated tokens on attempt ${iteration + 1} for $userWalletId", ) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index bf9014a844..97ba053720 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -13,7 +13,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.crypto.derivation.toMutable -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -154,11 +154,11 @@ class NetworkFactory @Inject constructor( private fun Blockchain.isBlockchainSupported(): Boolean { if (this == Blockchain.Unknown) { - Timber.w("Unable to convert Unknown blockchain to the domain network model") + TangemLogger.w("Unable to convert Unknown blockchain to the domain network model") return false } if (this in excludedBlockchains) { - Timber.w("Unable to convert excluded blockchain to the domain network model") + TangemLogger.w("Unable to convert excluded blockchain to the domain network model") return false } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt index 91f3f11426..a3c47ae0f1 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt @@ -15,11 +15,11 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.core.utils.eitherOn import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.joda.time.DateTime -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.seconds @@ -194,7 +194,7 @@ internal class DefaultQuotesFetcher( return this } - Timber.d("Some quotes are missing from the server response: $skippedIds") + TangemLogger.d("Some quotes are missing from the server response: $skippedIds") val emptyQuotes = skippedIds.associateWith { QuotesResponse.Quote.EMPTY } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt index 710a982835..a1b8af7782 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt @@ -1,10 +1,10 @@ package com.tangem.data.common.utils +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.yield -import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException @Suppress("UnconditionalJumpStatementInLoop", "MagicNumber") @@ -19,7 +19,7 @@ suspend fun retryOnError(priority: Boolean = false, startRetryDelay: Int = 5 currentCoroutineContext().ensureActive() } - Timber.e(e, "Error occurred during retryOnError block") + TangemLogger.e("Error occurred during retryOnError block", e) if (priority && priorityCounter > 0) { --priorityCounter diff --git a/data/earn/build.gradle.kts b/data/earn/build.gradle.kts index 985868438d..87173ffe7a 100644 --- a/data/earn/build.gradle.kts +++ b/data/earn/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) - implementation(deps.timber) implementation(tangemDeps.blockchain) // endregion diff --git a/data/express/build.gradle.kts b/data/express/build.gradle.kts index f75d1dde55..473892eedf 100644 --- a/data/express/build.gradle.kts +++ b/data/express/build.gradle.kts @@ -30,7 +30,6 @@ dependencies { /** Other */ implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt index 31d7577acf..360ebda6c1 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -12,7 +12,7 @@ import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.filterIf -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultExpressRepository( private val tangemExpressApi: TangemExpressApi, @@ -36,7 +36,7 @@ internal class DefaultExpressRepository( .filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes } }, onError = { error -> - Timber.w(error, "Unable to fetch express providers") + TangemLogger.w("Unable to fetch express providers", error) throw error }, ) diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt index 6e97e2c7b5..449959c978 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt @@ -22,11 +22,11 @@ import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.update -import timber.log.Timber import javax.inject.Inject typealias InitializationStatusFlow = MutableStateFlow>> @@ -85,7 +85,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor( if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) { initializationStatus.update { e.lceError() } } - Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}") + TangemLogger.e("Unable to fetch assets for: ${userWallet.walletId.stringValue}", e) throw e } } diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 08b86b9db4..c5ea5e524e 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { // endregion // Other libraries - implementation(deps.timber) // endregion // region Core modules diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index ca09522466..5f39f9f870 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -14,10 +14,10 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.version.AppVersionProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber import java.io.File /** @@ -102,7 +102,7 @@ internal class DefaultFeedbackRepository( override fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { return blockchainsErrors.value[userWalletId].also { - if (it == null) Timber.e("Blockchain error info is null for $userWalletId") + if (it == null) TangemLogger.e("Blockchain error info is null for $userWalletId") } } diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index ce17fcef05..fa149015fa 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -51,7 +51,6 @@ dependencies { /** Other */ implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index efdfc59129..c6316183e1 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -22,7 +22,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.crypto.BlockchainUtils -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class ManagedCryptoCurrencyFactory( private val networkFactory: NetworkFactory, @@ -223,7 +223,7 @@ internal class ManagedCryptoCurrencyFactory( blockchain.canHandleTokens() -> { val formattedContractAddress = blockchain.reformatContractAddress(contractAddress) if (formattedContractAddress == null) { - Timber.w("Couldn't reformat $contractAddress") + TangemLogger.w("Couldn't reformat $contractAddress") return null } SourceNetwork.Default( diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 1678a61ead..bce4fe8a70 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.jodatime) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) implementation(tangemDeps.blockchain) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index 90ca5576e0..d90e4e2a52 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi) - implementation(deps.timber) // endregion // region Tests diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt index 8c3fbae1a2..6585752203 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt @@ -3,7 +3,7 @@ package com.tangem.data.networks.converters import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.network.NetworkAddress import com.tangem.utils.converter.TwoWayConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Converter from [NetworkAddressConverter.Value] to [NetworkAddress] and vice versa @@ -58,7 +58,7 @@ internal object NetworkAddressConverter : TwoWayConverter + TangemLogger.e("Failed to fetch network status for $userWalletId [${network.rawId}]", throwable) networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) } } diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt index 8406342ea9..94e3c3b9cc 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -10,11 +10,11 @@ import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* -import timber.log.Timber /** * Default implementation of [MultiNetworkStatusProducer] @@ -43,7 +43,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( val userWallet = userWalletsListRepository.getSyncOrNull(params.userWalletId) if (userWallet == null) { - Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") + TangemLogger.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") return@mapNotNull null } diff --git a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt index 4755e55248..b0dc9c6af7 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt @@ -1,9 +1,9 @@ package com.tangem.data.networks.repository -import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.storeStatus import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.network.Network @@ -11,8 +11,8 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber /** * Default implementation of [NetworksRepository] @@ -36,8 +36,8 @@ internal class DefaultNetworksRepository( val currencies = runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } - .getOrElse { - Timber.e(it, "Unable to create wallet currencies") + .getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) return@withContext } @@ -50,8 +50,8 @@ internal class DefaultNetworksRepository( network: Network, ): List { return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } - .getOrElse { - Timber.e(it, "Unable to create wallet currencies") + .getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) return emptyList() } .map { currency -> @@ -67,8 +67,8 @@ internal class DefaultNetworksRepository( network: Network.RawID, ): List { return runCatching { cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) } - .getOrElse { - Timber.e(it, "Unable to create wallet currencies") + .getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) return emptyList() } .map { currency -> diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 0cb7dba076..05b161810f 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -7,15 +7,17 @@ import com.tangem.data.networks.converters.SimpleNetworkStatusConverter import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.StatusSource import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.* +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* -import timber.log.Timber +import kotlinx.coroutines.launch import java.io.File internal typealias WalletIdWithSimpleStatus = Map> @@ -45,7 +47,7 @@ internal class DefaultNetworksStatusesStore( oldFile.delete() } } catch (e: Exception) { - Timber.e(e, "Error while deleting old networks statuses datastore file") + TangemLogger.e("Error while deleting old networks statuses datastore file", e) } val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch @@ -89,7 +91,7 @@ internal class DefaultNetworksStatusesStore( ifNotFound: (Network.ID) -> SimpleNetworkStatus?, ) { if (networks.isEmpty()) { - Timber.d("Nothing to update: networks are empty") + TangemLogger.d("Nothing to update: networks are empty") return } diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt index b1da135fa9..6edd224074 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt @@ -5,7 +5,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Store actual network [status] by [userWalletId]. @@ -41,14 +41,14 @@ internal suspend fun NetworksStatusesStore.storeStatus(userWalletId: UserWalletI internal suspend fun NetworksStatusesStore.storeSuccess(userWalletId: UserWalletId, status: NetworkStatus) { if (status.value is NetworkStatus.Unreachable) { val message = "Use storeError method to save unreachable status" - Timber.d(message) + TangemLogger.d(message) error(message) } if (status.value.source != StatusSource.ACTUAL) { val message = "Method storeActual can be called only with StatusSource.ACTUAL" - Timber.d(message) + TangemLogger.d(message) error(message) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt b/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt index b395990e65..59058d09c2 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt @@ -8,10 +8,10 @@ import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber /** * Default implementation of [NetworksCleaner]. @@ -30,7 +30,7 @@ internal class DefaultNetworksCleaner( override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) { - Timber.d("No currencies to clear for wallet: $userWalletId") + TangemLogger.d("No currencies to clear for wallet: $userWalletId") return } @@ -49,7 +49,7 @@ internal class DefaultNetworksCleaner( runSuspendCatching { networksStatusesStore.clear(userWalletId = userWalletId, networks = networks) } - .onFailure { Timber.e(it, "Failed to clear network statuses for wallet: $userWalletId") } + .onFailure { TangemLogger.e("Failed to clear network statuses for wallet: $userWalletId", it) } } } @@ -63,7 +63,7 @@ internal class DefaultNetworksCleaner( walletManagersFacade.remove(userWalletId = userWalletId, networks = networks) } .onFailure { - Timber.e(it, "Failed to remove networks from Blockchain SDK for wallet: $userWalletId") + TangemLogger.e("Failed to remove networks from Blockchain SDK for wallet: $userWalletId", it) } } @@ -72,7 +72,7 @@ internal class DefaultNetworksCleaner( walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens) } .onFailure { - Timber.e(it, "Failed to remove tokens from Blockchain SDK for wallet: $userWalletId") + TangemLogger.e("Failed to remove tokens from Blockchain SDK for wallet: $userWalletId", it) } } } diff --git a/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt b/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt index 9c9742678c..e94de7efb5 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** Factory for creating [NetworkStatus] */ object NetworkStatusFactory { @@ -104,7 +104,7 @@ object NetworkStatusFactory { } if (amount == null) { - Timber.w("Unable to find amount for cryptocurrency: $currency") + TangemLogger.w("Unable to find amount for cryptocurrency: $currency") currency.id to NetworkStatus.Amount.NotFound } else { currency.id to NetworkStatus.Amount.Loaded(amount.value) @@ -129,7 +129,7 @@ object NetworkStatusFactory { } if (amount == null) { - Timber.w("Unable to find amount for cryptocurrency: $currency") + TangemLogger.w("Unable to find amount for cryptocurrency: $currency") currency.id to null } else { currency.id to amount.yieldSupplyStatus @@ -191,7 +191,7 @@ object NetworkStatusFactory { } if (address.value.isBlank()) { - Timber.w("Address value is blank") + TangemLogger.w("Address value is blank") } return NetworkAddress.Address(address.value, type) diff --git a/data/news/build.gradle.kts b/data/news/build.gradle.kts index 86796fcedd..0d06bf693d 100644 --- a/data/news/build.gradle.kts +++ b/data/news/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) - implementation(deps.timber) // endregion // region Tests diff --git a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt index 3e3ef70c45..293cc63837 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt @@ -28,9 +28,9 @@ import com.tangem.pagination.exception.EndOfPaginationException import com.tangem.pagination.fetcher.BatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import timber.log.Timber /** * Implementation of [NewsRepository]. @@ -72,7 +72,7 @@ internal class DefaultNewsRepository( response.items }, onError = { error -> - Timber.e(error, "Failed to get list of news") + TangemLogger.e("Failed to get list of news", error) throw error }, ) @@ -224,8 +224,7 @@ internal class DefaultNewsRepository( return withContext(dispatchers.io) { when (val apiResponse = newsApi.getTrendingNews(limit = limit, language = language)) { is ApiResponse.Error -> { - Timber.e( - apiResponse.cause, + TangemLogger.e( "Trending news fetch failed cause: ${ when (val error = apiResponse.cause) { is ApiResponseError.HttpException -> error.code @@ -234,6 +233,7 @@ internal class DefaultNewsRepository( is ApiResponseError.UnknownException -> "UnknownException" } }", + apiResponse.cause, ) trendingNewsStore.clear() trendingNewsStore.store( diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index f53acc71ce..63a318a877 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.arrow.fx) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.androidx.paging.runtime) implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 7b022349ef..3baa26a3f4 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -34,6 +34,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* @@ -42,7 +43,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -241,7 +241,7 @@ internal class DefaultNFTRepository @Inject constructor( // NFTCleaner implementation override suspend fun invoke(userWalletId: UserWalletId, networks: Set) { if (networks.isEmpty()) { - Timber.d("No networks to clear for wallet: $userWalletId") + TangemLogger.d("No networks to clear for wallet: $userWalletId") return } @@ -252,7 +252,7 @@ internal class DefaultNFTRepository @Inject constructor( // nftRuntimeStoreFactory.provide(network = network).clear() } .onFailure { throwable -> - Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId") + TangemLogger.e("Failed to clear NFT data for network $network for wallet: $userWalletId", throwable) } } } diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index bf0adddbac..c61541ce6a 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -47,7 +47,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) implementation(deps.kotlin.serialization) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 5f768924a0..e7f7ad38db 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -22,19 +22,15 @@ import com.tangem.domain.card.common.extensions.canHandleToken import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.common.wallets.loadAndGet -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency import com.tangem.domain.onramp.repositories.HotCryptoRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.runCatching -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.plus -import timber.log.Timber /** * Default implementation of [HotCryptoRepository] @@ -123,9 +119,9 @@ internal class DefaultHotCryptoRepository( return runCatching(dispatchers.io) { tangemTechApi.getHotCrypto(currencyId = appCurrencyId).getOrThrow() } - .onSuccess { Timber.d("HotCrypto is successfully updated") } + .onSuccess { TangemLogger.d("HotCrypto is successfully updated") } .onFailure { throwable -> - Timber.e(throwable, "Unable to fetch hot crypto") + TangemLogger.e("Unable to fetch hot crypto", throwable) val httpException = throwable as? ApiResponseError.HttpException analyticsEventHandler.send( diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index bf07c9a118..7fe880aa2b 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -50,6 +50,7 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -57,7 +58,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.joda.time.DateTime -import timber.log.Timber import java.util.UUID @Suppress("LongParameterList", "LargeClass", "TooManyFunctions") @@ -221,7 +221,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch onramp payment methods") + TangemLogger.w("Unable to fetch onramp payment methods", error) throw error }, ) @@ -256,7 +256,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch onramp pairs") + TangemLogger.w("Unable to fetch onramp pairs", error) throw error }, ) @@ -273,7 +273,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch express providers") + TangemLogger.w("Unable to fetch express providers", error) throw error }, ) @@ -321,7 +321,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch onramp pairs") + TangemLogger.w("Unable to fetch onramp pairs", error) throw error }, ) @@ -455,7 +455,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { e -> - Timber.e(e) + TangemLogger.e("Error", e) throw e }, ) @@ -478,7 +478,7 @@ internal class DefaultOnrampRepository( throw OnrampRedirectError.VerificationFailed } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) throw e } } @@ -602,7 +602,7 @@ internal class DefaultOnrampRepository( countryCode = countryCode, ) } else { - Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") + TangemLogger.w("Unable to fetch onramp quotes for ${provider.id}. $error", error) OnrampQuote.Error( paymentMethod = paymentMethod, provider = provider, @@ -612,7 +612,7 @@ internal class DefaultOnrampRepository( ) } } else { - Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") + TangemLogger.w("Unable to fetch onramp quotes for ${provider.id}. $error", error) null } diff --git a/data/promo/build.gradle.kts b/data/promo/build.gradle.kts index b4ab572c21..e4e45ecdca 100644 --- a/data/promo/build.gradle.kts +++ b/data/promo/build.gradle.kts @@ -14,7 +14,6 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index b4f0191e43..5d169e0702 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) - implementation(deps.timber) // endregion // region Tests diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index 1660472cb9..6b1099fb1a 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -13,7 +13,7 @@ import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -37,7 +37,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( override suspend fun invoke(params: MultiQuoteStatusFetcher.Params) = Either.catchOn(dispatchers.default) { if (params.currenciesIds.isEmpty()) { - Timber.d("No currencies to fetch quotes for") + TangemLogger.d("No currencies to fetch quotes for") return@catchOn } @@ -67,7 +67,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( quotesStatusesStore.store(values = updatedResponse.quotes) } .onLeft { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } @@ -77,7 +77,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( if (appCurrencyId.isNullOrBlank()) { val exception = IllegalStateException("Unable to get AppCurrency for updating quotes") - Timber.e(exception) + TangemLogger.e("Error", exception) throw exception } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index f499f8a4a0..cad0c16d50 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -4,17 +4,17 @@ import androidx.annotation.VisibleForTesting import arrow.core.left import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* -import timber.log.Timber /** * Default implementation of [MultiQuoteUpdater] which updates quotes when the app currency changes @@ -36,14 +36,14 @@ internal class DefaultMultiQuoteUpdater( private val updaterHolder = JobHolder() override fun subscribe() { - Timber.d("Subscribe on quotes updates") + TangemLogger.d("Subscribe on quotes updates") getMultiQuoteUpdates() .launchIn(coroutineScope) .saveIn(updaterHolder) } override fun unsubscribe() { - Timber.e("Unsubscribe from quotes updates") + TangemLogger.i("Unsubscribe from quotes updates") updaterHolder.cancel() } @@ -63,10 +63,10 @@ internal class DefaultMultiQuoteUpdater( appCurrencyId = appCurrency.id, ), ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } } .retryWhen { cause, _ -> - Timber.e("Retry updating quotes: $cause") + TangemLogger.e("Retry updating quotes: $cause") emit(cause.left()) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt index d170b82c5c..d16b775518 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -4,7 +4,7 @@ import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Default implementation of [QuotesRepository] @@ -19,7 +19,7 @@ internal class DefaultQuotesRepository( override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set { if (currenciesIds.isEmpty()) { - Timber.e("currenciesIds are empty") + TangemLogger.e("currenciesIds are empty") return emptySet() } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index 3225adfea6..9c4bd521e7 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt @@ -4,16 +4,16 @@ import androidx.datastore.core.DataStore import com.tangem.data.quotes.converter.QuoteStatusConverter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import timber.log.Timber internal typealias CurrencyIdWithQuote = Map @@ -64,7 +64,7 @@ internal class DefaultQuotesStatusesStore( ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, ) { if (currenciesIds.isEmpty()) { - Timber.d("Nothing to update: currencies ids are empty") + TangemLogger.d("Nothing to update: currencies ids are empty") return } diff --git a/data/search/.gitignore b/data/search/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/search/build.gradle.kts b/data/search/build.gradle.kts new file mode 100644 index 0000000000..96eb497c11 --- /dev/null +++ b/data/search/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.search" +} + +dependencies { + // region Project - Core + implementation(projects.core.datasource) + api(projects.core.utils) + // endregion + + // region Project - Data + implementation(projects.data.common) + // endregion + + // region Project - Domain + implementation(projects.domain.search) + implementation(projects.domain.common) + implementation(projects.domain.account.status) + implementation(projects.domain.markets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Other libraries + implementation(deps.androidx.datastore) + implementation(deps.moshi.kotlin) + // endregion +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt new file mode 100644 index 0000000000..60bedf12bd --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.data.search.converter + +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.TextHintDTO +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import com.tangem.utils.converter.Converter + +internal class TextHintDTOToSearchTextHintConverter : Converter { + override fun convert(value: TextHintDTO): SearchTextHint { + return SearchTextHint( + text = value.text, + timestamp = value.timestamp, + ) + } +} + +internal class RecentTokenDTOToRecentSearchTokenConverter : Converter { + override fun convert(value: RecentTokenDTO): RecentSearchToken { + return RecentSearchToken( + id = CryptoCurrency.RawID(value.id), + name = value.name, + symbol = value.symbol, + imageUrl = value.imageUrl, + timestamp = value.timestamp, + ) + } +} + +internal class RecentSearchTokenToRecentTokenDTOConverter : Converter { + override fun convert(value: RecentSearchToken): RecentTokenDTO { + return RecentTokenDTO( + id = value.id.value, + name = value.name, + symbol = value.symbol, + imageUrl = value.imageUrl, + timestamp = value.timestamp, + ) + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt b/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt new file mode 100644 index 0000000000..e4a321d4da --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt @@ -0,0 +1,98 @@ +package com.tangem.data.search.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.data.search.model.SearchHistoryDTO +import com.tangem.data.search.repository.DefaultSearchRepository +import com.tangem.data.search.store.DefaultSearchHistoryStore +import com.tangem.data.search.store.SearchHistoryStore +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.search.repository.SearchRepository +import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase +import com.tangem.domain.search.usecase.GetSearchResultsUseCase +import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase +import com.tangem.domain.search.usecase.SaveSearchQueryUseCase +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SearchDataModule { + + @OptIn(ExperimentalStdlibApi::class) + @Provides + @Singleton + fun provideSearchHistoryDataStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appScope: AppCoroutineScope, + ): DataStore { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + defaultValue = SearchHistoryDTO(), + adapter = moshi.adapter(), + ), + produceFile = { context.dataStoreFile(fileName = "search_history") }, + scope = appScope, + ) + } + + @Provides + @Singleton + fun provideSearchHistoryStore(dataStore: DataStore): SearchHistoryStore { + return DefaultSearchHistoryStore(dataStore = dataStore) + } + + @Provides + @Singleton + fun provideSearchRepository( + store: SearchHistoryStore, + dispatchers: CoroutineDispatcherProvider, + ): SearchRepository { + return DefaultSearchRepository( + store = store, + dispatchers = dispatchers, + ) + } + + @Provides + fun provideGetSearchResultsUseCase( + searchRepository: SearchRepository, + multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + userWalletsListRepository: UserWalletsListRepository, + ): GetSearchResultsUseCase { + return GetSearchResultsUseCase( + searchRepository = searchRepository, + multiAccountStatusListSupplier = multiAccountStatusListSupplier, + userWalletsListRepository = userWalletsListRepository, + ) + } + + @Provides + fun provideSaveSearchQueryUseCase(searchRepository: SearchRepository): SaveSearchQueryUseCase { + return SaveSearchQueryUseCase(searchRepository = searchRepository) + } + + @Provides + fun provideSaveRecentSearchTokenUseCase(searchRepository: SearchRepository): SaveRecentSearchTokenUseCase { + return SaveRecentSearchTokenUseCase(searchRepository = searchRepository) + } + + @Provides + fun provideClearSearchHistoryUseCase(searchRepository: SearchRepository): ClearSearchHistoryUseCase { + return ClearSearchHistoryUseCase(searchRepository = searchRepository) + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt b/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt new file mode 100644 index 0000000000..3166dd4d71 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt @@ -0,0 +1,24 @@ +package com.tangem.data.search.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchHistoryDTO( + val textHints: List = emptyList(), + val recentTokens: List = emptyList(), +) + +@JsonClass(generateAdapter = true) +internal data class TextHintDTO( + val text: String, + val timestamp: Long, +) + +@JsonClass(generateAdapter = true) +internal data class RecentTokenDTO( + val id: String, + val name: String, + val symbol: String, + val imageUrl: String?, + val timestamp: Long, +) \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt b/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt new file mode 100644 index 0000000000..def344a019 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt @@ -0,0 +1,60 @@ +package com.tangem.data.search.repository + +import com.tangem.data.search.converter.RecentSearchTokenToRecentTokenDTOConverter +import com.tangem.data.search.converter.RecentTokenDTOToRecentSearchTokenConverter +import com.tangem.data.search.converter.TextHintDTOToSearchTextHintConverter +import com.tangem.data.search.model.TextHintDTO +import com.tangem.data.search.store.SearchHistoryStore +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import com.tangem.domain.search.repository.SearchRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +internal class DefaultSearchRepository( + private val store: SearchHistoryStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SearchRepository { + + private val textHintConverter by lazy { + TextHintDTOToSearchTextHintConverter() + } + private val recentTokenConverter by lazy { + RecentTokenDTOToRecentSearchTokenConverter() + } + private val recentTokenDMConverter by lazy { + RecentSearchTokenToRecentTokenDTOConverter() + } + + override fun getTextHints(): Flow> { + return store.getTextHints() + .map { textHintConverter.convertList(it) } + .flowOn(dispatchers.io) + } + + override fun getRecentTokens(): Flow> { + return store.getRecentTokens() + .map { recentTokenConverter.convertList(it) } + .flowOn(dispatchers.io) + } + + override suspend fun saveTextHint(text: String) = withContext(dispatchers.io) { + store.saveTextHint( + TextHintDTO( + text = text, + timestamp = System.currentTimeMillis(), + ), + ) + } + + override suspend fun saveRecentToken(token: RecentSearchToken) = withContext(dispatchers.io) { + store.saveRecentToken(recentTokenDMConverter.convert(token)) + } + + override suspend fun clearHistory() = withContext(dispatchers.io) { + store.clearAll() + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt b/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt new file mode 100644 index 0000000000..348328faa1 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt @@ -0,0 +1,53 @@ +package com.tangem.data.search.store + +import androidx.datastore.core.DataStore +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.SearchHistoryDTO +import com.tangem.data.search.model.TextHintDTO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class DefaultSearchHistoryStore( + private val dataStore: DataStore, +) : SearchHistoryStore { + + override fun getTextHints(): Flow> { + return dataStore.data.map { it.textHints.sortedByDescending(TextHintDTO::timestamp) } + } + + override fun getRecentTokens(): Flow> { + return dataStore.data.map { it.recentTokens.sortedByDescending(RecentTokenDTO::timestamp) } + } + + override suspend fun saveTextHint(hint: TextHintDTO) { + dataStore.updateData { current -> + val updated = current.textHints + .filter { it.text != hint.text } + .toMutableList() + .apply { add(0, hint) } + .take(MAX_HISTORY_SIZE) + current.copy(textHints = updated) + } + } + + override suspend fun saveRecentToken(token: RecentTokenDTO) { + dataStore.updateData { current -> + val updated = current.recentTokens + .filter { it.id != token.id } + .toMutableList() + .apply { add(0, token) } + .take(MAX_HISTORY_SIZE) + current.copy(recentTokens = updated) + } + } + + override suspend fun clearAll() { + dataStore.updateData { + SearchHistoryDTO() + } + } + + private companion object { + const val MAX_HISTORY_SIZE = 3 + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt b/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt new file mode 100644 index 0000000000..02b398d0e5 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt @@ -0,0 +1,13 @@ +package com.tangem.data.search.store + +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.TextHintDTO +import kotlinx.coroutines.flow.Flow + +internal interface SearchHistoryStore { + fun getTextHints(): Flow> + fun getRecentTokens(): Flow> + suspend fun saveTextHint(hint: TextHintDTO) + suspend fun saveRecentToken(token: RecentTokenDTO) + suspend fun clearAll() +} \ No newline at end of file diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 30c7231276..3b71894edc 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -33,7 +33,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 588d3b71e3..0f77d6749a 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -14,11 +14,11 @@ import com.tangem.domain.settings.usercountry.models.GB_COUNTRY import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.Locale @Suppress("TooManyFunctions") @@ -145,7 +145,7 @@ internal class DefaultSettingsRepository( override fun getUserCountryCode(): StateFlow = userCountryFlow override suspend fun fetchUserCountryCode() { - Timber.i("Start fetching user country code") + TangemLogger.i("Start fetching user country code") // for GB locale avoid request geo and use device default (FCA fixes) if (Locale.getDefault().country == GB_COUNTRY.code) { @@ -166,7 +166,7 @@ internal class DefaultSettingsRepository( else -> UserCountry.Other(code = country) } - Timber.i("User code country is $code") + TangemLogger.i("User code country is $code") userCountryFlow.value = code } diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index fc7059b626..3aa93ac8e2 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) implementation(deps.firebase.crashlytics) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index 8752d83b0e..b189675a38 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -5,11 +5,7 @@ import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensure -import com.tangem.data.staking.converters.ethpool.P2PEthPoolBroadcastResultConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolErrorConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingAccountConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolUnsignedTxConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolVaultConverter +import com.tangem.data.staking.converters.ethpool.* import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest @@ -28,11 +24,11 @@ import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber /** * P2PEthPool staking repository implementation @@ -71,7 +67,7 @@ internal class DefaultP2PEthPoolRepository( override suspend fun fetchVaults(network: P2PEthPoolNetwork) { val vaults = if (stakingFeatureToggles.isEthStakingEnabled) { getVaults(network).getOrElse { error -> - Timber.e("Error fetching P2PEthPool vaults: $error") + TangemLogger.e("Error fetching P2PEthPool vaults: $error") emptyList() } } else { diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt index 171c5c720a..79dc265b4f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt @@ -47,13 +47,13 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakeKitRepository( @@ -91,7 +91,7 @@ internal class DefaultStakeKitRepository( when (response) { is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true } is ApiResponse.Error -> { - Timber.e("Error fetching enabled yields: ${response.cause}") + TangemLogger.e("Error fetching enabled yields: ${response.cause}") emptyList() } } @@ -142,7 +142,7 @@ internal class DefaultStakeKitRepository( network = networkTypeString, status = actionStatusString, ).getOrThrow().data, - onError = { Timber.e("Error converting staking actions list: $it") }, + onError = { TangemLogger.e("Error converting staking actions list: $it") }, ) } } @@ -348,7 +348,7 @@ internal class DefaultStakeKitRepository( private suspend fun getEnabledYieldsSync(): List { return YieldConverter.convertListIgnoreErrors( input = stakingYieldsStore.getSync(), - onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + onError = { TangemLogger.e("Error converting one of the items in enabled yields: $it") }, ) } @@ -356,7 +356,7 @@ internal class DefaultStakeKitRepository( return stakingYieldsStore.get().map { yields -> YieldConverter.convertListIgnoreErrors( input = yields, - onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + onError = { TangemLogger.e("Error converting one of the items in enabled yields: $it") }, ) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt index 0147b2da07..b0c1a1ad67 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt @@ -1,15 +1,15 @@ package com.tangem.data.staking import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.api.stakekit.models.request.* +import com.tangem.datasource.api.stakekit.models.request.SubmitTransactionHashRequestBody import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber internal class DefaultStakeKitTransactionHashRepository( private val stakeKitApi: StakeKitApi, @@ -75,7 +75,7 @@ internal class DefaultStakeKitTransactionHashRepository( append("StakeKit id = ${transaction.transactionId} and\n") append("transaction hash = ${transaction.transactionHash}") } - Timber.e(logMessage) + TangemLogger.e(logMessage) } } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt index c957ac3d12..d329e2bb77 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt @@ -30,11 +30,11 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject /** @@ -66,10 +66,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( ) : MultiStakingBalanceFetcher { override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either { - Timber.i("Start fetching staking balances for params:\n$params") + TangemLogger.i("Start fetching staking balances for params:\n$params") val stakingIds = params.stakingIds.ifEmpty { - Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") + TangemLogger.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") return Unit.right() } @@ -84,7 +84,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( stakingIntegrationID is StakingIntegrationID.StakeKit } - Timber.i( + TangemLogger.i( """ Staking IDs to fetch: - StakeKit: ${stakeKitIds.joinToString()} @@ -104,7 +104,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } } .onLeft { throwable -> - Timber.e(throwable, "Unable to fetch staking balances $params") + TangemLogger.e("Unable to fetch staking balances $params", throwable) if (stakeKitIds.isNotEmpty()) { stakeKitBalancesStore.storeError( @@ -137,7 +137,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( val vaults = runSuspendCatching { p2pEthPoolVaultsStore.getSync() }.getOrNull().orEmpty() if (vaults.isEmpty()) { - Timber.w("No P2PEthPool vaults available for $userWalletId, storing empty balances") + TangemLogger.w("No P2PEthPool vaults available for $userWalletId, storing empty balances") p2PEthPoolBalancesStore.storeEmpty(userWalletId = userWalletId, stakingIds = stakingIds) return } @@ -155,7 +155,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( val addresses = stakingIds.map { it.address }.toSet() val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses) - Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId") + TangemLogger.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId") if (responses.isNotEmpty()) { p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses) @@ -167,19 +167,22 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } if (missingStakingIds.isNotEmpty()) { - Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds") + TangemLogger.i( + "Missing responses for ${missingStakingIds.size} staking IDs:" + + " $missingStakingIds", + ) p2PEthPoolBalancesStore.storeError( userWalletId = userWalletId, stakingIds = missingStakingIds.toSet(), ) } } else { - Timber.i("No P2PEthPool responses received for $userWalletId") + TangemLogger.i("No P2PEthPool responses received for $userWalletId") p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) } }, onError = { throwable -> - Timber.e(throwable, "Unable to fetch P2PEthPool balances $userWalletId") + TangemLogger.e("Unable to fetch P2PEthPool balances $userWalletId", throwable) p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) @@ -207,7 +210,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( is ApiResponse.Success -> { val data = response.data if (data.error != null) { - Timber.w( + TangemLogger.w( "P2PEthPool API returned error for vault ${vault.vaultAddress}, " + "address $address: ${data.error ?: "error"}", ) @@ -219,17 +222,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } } is ApiResponse.Error -> { - Timber.w( - response.cause, + TangemLogger.w( "Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " + "address $address", + response.cause, ) } } }.onFailure { error -> - Timber.w( - error, + TangemLogger.w( "Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address", + error, ) } } @@ -245,7 +248,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( if (!isSupportedByWallet) { val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet") - Timber.e(exception) + TangemLogger.e("Error", exception) ifNotSupported(exception) } @@ -263,7 +266,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( val availableStakingIds = groupedStakingIds[true].orEmpty() val unavailableStakingIds = groupedStakingIds[false].orEmpty() - Timber.i( + TangemLogger.i( """ Available staking IDs: ${availableStakingIds.joinToString()} Unavailable staking IDs: ${unavailableStakingIds.joinToString()} @@ -282,7 +285,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( – stakingIds: ${stakingIds.joinToString()} """.trimIndent(), ) - Timber.i(exception) + TangemLogger.i(exception.toString()) throw exception } } @@ -293,7 +296,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( if (yieldsIds.isEmpty()) { val exception = IllegalStateException("No enabled yields for $userWalletId") - Timber.e(exception) + TangemLogger.e("Error", exception) throw exception } @@ -320,7 +323,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( .toSet() } - Timber.i( + TangemLogger.i( "Successfully fetched staking balances for $userWalletId:\n${yieldBalances.joinToString("\n")}", ) stakeKitBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) @@ -337,7 +340,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } }, onError = { throwable -> - Timber.e(throwable, "Unable to fetch staking balances $userWalletId") + TangemLogger.e("Unable to fetch staking balances $userWalletId", throwable) stakeKitBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt index bf66f62a4a..5f96221f71 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt @@ -10,13 +10,13 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.domain.staking.single.SingleStakingBalanceProducer.Companion.selectStakingBalance import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import timber.log.Timber /** * Default implementation of [SingleStakingBalanceProducer] @@ -39,7 +39,7 @@ internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor( override val fallback: Option = StakingBalance.Error(stakingId = params.stakingId).some() override fun produce(): Flow { - Timber.i("Producing staking balance for params:\n$params") + TangemLogger.i("Producing staking balance for params:\n$params") return multiStakingBalanceSupplier( params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId), diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt index 0fe16ead81..696578c106 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt @@ -9,10 +9,10 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber /** * Default implementation of [StakingCleaner]. @@ -32,7 +32,7 @@ internal class DefaultStakingCleaner( override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) { - Timber.d("No currencies to clear for wallet: $userWalletId") + TangemLogger.d("No currencies to clear for wallet: $userWalletId") return } @@ -41,7 +41,7 @@ internal class DefaultStakingCleaner( } if (stakingIds.isEmpty()) { - Timber.d("All currencies have no stakingIds to clear for wallet: $userWalletId") + TangemLogger.d("All currencies have no stakingIds to clear for wallet: $userWalletId") return } @@ -50,7 +50,7 @@ internal class DefaultStakingCleaner( override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set) { if (stakingIds.isEmpty()) { - Timber.d("No stakingIds to clear for wallet: $userWalletId") + TangemLogger.d("No stakingIds to clear for wallet: $userWalletId") return } @@ -66,13 +66,13 @@ internal class DefaultStakingCleaner( runSuspendCatching { stakeKitBalancesStore.clear(userWalletId, stakingIds) } - .onFailure { Timber.e(it, "Failed to clear StakeKit balance statuses for wallet: $userWalletId") } + .onFailure { TangemLogger.e("Failed to clear StakeKit balance statuses for wallet: $userWalletId", it) } } private suspend fun clearP2PEthPoolBalancesStore(userWalletId: UserWalletId, stakingIds: Set) { runSuspendCatching { p2pEthPoolBalancesStore.clear(userWalletId, stakingIds) } - .onFailure { Timber.e(it, "Failed to clear P2PEthPool balance statuses for wallet: $userWalletId") } + .onFailure { TangemLogger.e("Failed to clear P2PEthPool balance statuses for wallet: $userWalletId", it) } } } \ No newline at end of file diff --git a/data/swap/build.gradle.kts b/data/swap/build.gradle.kts index a7ea5bd55c..8b9fdaffe8 100644 --- a/data/swap/build.gradle.kts +++ b/data/swap/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index e4bbdeebee..d51b43b1e1 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -15,6 +15,8 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi @@ -35,11 +37,11 @@ import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber import java.io.IOException import java.math.BigDecimal import java.util.UUID @@ -98,8 +100,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } val mappedProviders = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { expressProviders[it.providerId] } + .mapNotNull { it.withExpressProvider(expressProviders) } .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { @@ -157,8 +158,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) val mappedProvider = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { mappedProviders[it.providerId] } + .mapNotNull { it.withExpressProvider(mappedProviders) } .filterYieldSupplyProvider(currencyStatusFrom) if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { @@ -221,6 +221,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( toTokenAmount = toTokenAmount, fromTokenAmount = fromTokenAmount, allowanceContract = response.allowanceContract, + quoteId = response.quoteId, ) } @@ -235,6 +236,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String?, ): SwapDataModel = withContext(coroutineDispatcher.io) { val requestId = UUID.randomUUID().toString() val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus @@ -273,6 +275,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( appPreferencesStore = appPreferencesStore, ), toExtraId = toExtraId?.ifEmpty { null }, + quoteId = quoteId, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { @@ -392,7 +395,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ).getOrThrow() }, onError = { error -> - Timber.w(error, "Unable to get pairs") + TangemLogger.w("Unable to get pairs", error) throw error }, ) @@ -438,11 +441,23 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( return try { txDetailsMoshiAdapter.fromJson(txDetailsJson) } catch (e: IOException) { - Timber.e(e, "error parsing txDetailsJson") + TangemLogger.e("error parsing txDetailsJson", e) null } } + private fun SwapPairProvider.withExpressProvider( + expressProviders: Map, + ): ExpressProvider? { + val provider = expressProviders[providerId] ?: return null + return provider.copy(rateTypes = rateTypes.map { it.toExpressRateType() }) + } + + private fun RateType.toExpressRateType(): ExpressRateType = when (this) { + RateType.FLOAT -> ExpressRateType.Float + RateType.FIXED -> ExpressRateType.Fixed + } + private fun CryptoCurrency.getContractAddress(): String { return when (this) { is CryptoCurrency.Token -> this.contractAddress diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 96158e5dfb..f25b6ec88e 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -72,7 +72,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi.kotlin) implementation(deps.retrofit) // For HttpException - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt index 4d0cf9c1fc..fe43c7fe58 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt @@ -8,10 +8,10 @@ import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber /** * Responsible for merging custom tokens into a user's token response. @@ -105,7 +105,7 @@ internal class CustomTokensMerger( ).bind() }, onError = { error -> - Timber.e(error, "Unable to fetch token:\n$token") + TangemLogger.e("Unable to fetch token:\n$token", error) null }, ) diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 7becb2b968..a372c747a8 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other */ - implementation(deps.timber) /** tests */ testImplementation(projects.common.test) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 06b461cbd4..ba4141909d 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -33,8 +33,8 @@ import com.tangem.domain.transaction.models.EventTransactionTypeDto import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger @@ -271,7 +271,7 @@ internal class DefaultTransactionRepository( Result.failure(ex) } } else { - Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") + TangemLogger.e("${walletManager?.wallet?.blockchain} does not support transaction validation") Result.success(Unit) } } @@ -441,14 +441,14 @@ internal class DefaultTransactionRepository( val response = tangemTechApi.transactionEvents(body) response.fold( onSuccess = { - Timber.d("Successfully sent yield supply transaction hash: $hash") + TangemLogger.d("Successfully sent yield supply transaction hash: $hash") }, onError = { error -> - Timber.e(error, "Failed to send yield supply transaction hash: $hash") + TangemLogger.e("Failed to send yield supply transaction hash: $hash", error) }, ) }.onFailure { error -> - Timber.e(error, "Failed to send yield supply transaction hash: $hash") + TangemLogger.e("Failed to send yield supply transaction hash: $hash", error) } } @@ -460,7 +460,7 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) val preparer = walletManager as? TransactionPreparer ?: run { - Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder") + TangemLogger.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder") error("Wallet manager does not support TransactionPreparer") } return preparer diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 78b3545254..17ca52c9ce 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) - implementation(deps.timber) implementation(deps.jodatime) implementation(tangemDeps.blockchain) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 23761f8743..1763c2dd4b 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -20,9 +20,9 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.utils.SdkPageConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext -import timber.log.Timber class DefaultTxHistoryRepository( private val cacheRegistry: CacheRegistry, @@ -110,7 +110,7 @@ class DefaultTxHistoryRepository( ) ?.items.orEmpty() } catch (e: Throwable) { - Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}") + TangemLogger.e("Unable to load the transaction history for the requested page: ${Page.Initial}", e) emptyList() } } diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt index 972344ad6e..88b9b82391 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt @@ -16,7 +16,7 @@ import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListSource import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class RefactoredTxHistoryRepository( private val walletManagersFacade: WalletManagersFacade, @@ -96,16 +96,17 @@ internal class RefactoredTxHistoryRepository( .filterIfTxAlreadyAdded(apiItems = items) return if (recentItems.isEmpty()) { - Timber.d("Nothing to add to TxHistory") + TangemLogger.d("Nothing to add to TxHistory") this } else { - Timber.d( - "Recent transactions were added to TxHistory: %s", - recentItems.joinToString( - prefix = "[", - postfix = "]", - transform = TxInfo::txHash, - ), + TangemLogger.d( + "Recent transactions were added to TxHistory: ${ + recentItems.joinToString( + prefix = "[", + postfix = "]", + transform = TxInfo::txHash, + ) + }", ) return copy(items = recentItems + items) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index 2f67ef406b..c95c0761bb 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -11,7 +11,7 @@ import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.utils.SdkPageConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class TxHistoryPagingSource( private val sourceParams: Params, @@ -44,7 +44,7 @@ internal class TxHistoryPagingSource( } LoadResult.Page(wrappedItems.items, prevKey = null, nextKey = nextKey) } catch (e: Throwable) { - Timber.e(e, "Unable to load the transaction history for the requested page: $pageToLoad") + TangemLogger.e("Unable to load the transaction history for the requested page: $pageToLoad", e) LoadResult.Error(e) } @@ -89,16 +89,17 @@ internal class TxHistoryPagingSource( .filterIfTxAlreadyAdded(apiItems = items) return if (recentItems.isEmpty()) { - Timber.d("Nothing to add to TxHistory") + TangemLogger.d("Nothing to add to TxHistory") this } else { - Timber.d( - "Recent transactions were added to TxHistory: %s", - recentItems.joinToString( - prefix = "[", - postfix = "]", - transform = TxInfo::txHash, - ), + TangemLogger.d( + "Recent transactions were added to TxHistory: ${ + recentItems.joinToString( + prefix = "[", + postfix = "]", + transform = TxInfo::txHash, + ) + }", ) return copy(items = recentItems + items) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ef2ebc1613..80f1139140 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.arrow.fx) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.androidx.paging.runtime) implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt index 505383a099..48607769ea 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt @@ -8,10 +8,11 @@ import com.tangem.core.error.UniversalError import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory import com.tangem.data.pay.util.TangemPayErrorConverter +import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory" @@ -53,7 +54,28 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( decimals = TOKEN_DECIMALS, ) }.mapLeft { exception -> - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) + errorConverter.convert(exception) + } + } + + override fun create(userWallet: UserWallet): Either { + return catch { + val network = networkFactory.create( + blockchain = VisaUtilities.visaBlockchain, + extraDerivationPath = null, + userWallet = userWallet, + ) + cryptoCurrencyFactory.createToken( + network = requireNotNull(network), + rawId = CryptoCurrency.RawID(TOKEN_ID), + name = TOKEN_NAME, + symbol = TOKEN_NAME, + contractAddress = TOKEN_CONTRACT_ADDRESS, + decimals = TOKEN_DECIMALS, + ) + }.mapLeft { exception -> + TangemLogger.withTag(TAG).e("Error", exception) errorConverter.convert(exception) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt deleted file mode 100644 index db6fbcca4f..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.data.pay.converter - -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM -import com.tangem.domain.models.StatusSource -import com.tangem.domain.pay.PaymentAccountStatus -import com.tangem.utils.converter.TwoWayConverter - -/** - * Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM]. - * - * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted - * (Loading, ExposedDevice, Unavailable, NotSynced). - * - * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. - */ -internal object PaymentAccountStatusDMConverter : - TwoWayConverter { - - override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? { - return when (value) { - is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated() - is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus) - is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard() - is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked() - is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded( - cardId = value.cardId, - lastFourDigits = value.lastFourDigits, - balance = value.balance, - currencyCode = value.currencyCode, - depositAddress = value.depositAddress, - isPinSet = value.isPinSet, - ) - is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed() - // Transient statuses are not persisted - is PaymentAccountStatus.Loading, - is PaymentAccountStatus.Error.ExposedDevice, - is PaymentAccountStatus.Error.Unavailable, - is PaymentAccountStatus.Error.NotSynced, - -> null - } - } - - override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus { - return when (value) { - is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed - is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated - is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE) - is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE) - is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview( - source = StatusSource.CACHE, - kycStatus = value.kycStatus, - ) - is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded( - source = StatusSource.CACHE, - cardId = value.cardId, - lastFourDigits = value.lastFourDigits, - balance = value.balance, - currencyCode = value.currencyCode, - depositAddress = value.depositAddress, - isPinSet = value.isPinSet, - ) - null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE) - } - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt new file mode 100644 index 0000000000..a42852436a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.data.pay.converter + +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.utils.converter.TwoWayConverter + +/** + * Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM]. + * + * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted + * (Loading, ExposedDevice, Unavailable, NotSynced). + * + * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. + */ +internal object PaymentAccountStatusValueDMConverter : + TwoWayConverter { + + override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { + return when (value) { + is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated() + is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview( + kycStatus = value.kycStatus, + customerId = value.customerId, + ) + is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard() + is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard( + isLocked = true, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDM(), + cryptoBalance = value.cryptoBalance.toDM(), + ) + is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard( + isLocked = false, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDM(), + cryptoBalance = value.cryptoBalance.toDM(), + ) + is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed( + customerId = value.customerId, + ) + // Transient statuses are not persisted + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.Error.Unavailable, + is PaymentAccountStatusValue.Error.NotSynced, + -> null + } + } + + override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { + return when (value) { + is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated + is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed( + customerId = value.customerId, + ) + is PaymentAccountStatusValueDM.IssuingCard -> PaymentAccountStatusValue.IssuingCard( + source = StatusSource.CACHE, + ) + is PaymentAccountStatusValueDM.ActiveCard -> if (value.isLocked) { + PaymentAccountStatusValue.Locked( + source = StatusSource.CACHE, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDomain(), + cryptoBalance = value.cryptoBalance.toDomain(), + ) + } else { + PaymentAccountStatusValue.Loaded( + source = StatusSource.CACHE, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDomain(), + cryptoBalance = value.cryptoBalance.toDomain(), + ) + } + is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( + source = StatusSource.CACHE, + kycStatus = value.kycStatus, + customerId = value.customerId, + ) + null -> PaymentAccountStatusValue.Error.Unavailable + } + } + + private fun PaymentAccountStatusValue.FiatBalance.toDM(): PaymentAccountStatusValueDM.FiatBalanceDM { + return PaymentAccountStatusValueDM.FiatBalanceDM( + availableBalance = availableBalance, + currency = currency, + ) + } + + private fun PaymentAccountStatusValue.CryptoBalance.toDM(): PaymentAccountStatusValueDM.CryptoBalanceDM { + return PaymentAccountStatusValueDM.CryptoBalanceDM( + id = id, + chainId = chainId, + depositAddress = depositAddress, + tokenContractAddress = tokenContractAddress, + balance = balance, + ) + } + + private fun PaymentAccountStatusValueDM.FiatBalanceDM.toDomain(): PaymentAccountStatusValue.FiatBalance { + return PaymentAccountStatusValue.FiatBalance( + availableBalance = availableBalance, + currency = currency, + ) + } + + private fun PaymentAccountStatusValueDM.CryptoBalanceDM.toDomain(): PaymentAccountStatusValue.CryptoBalance { + return PaymentAccountStatusValue.CryptoBalance( + id = id, + chainId = chainId, + depositAddress = depositAddress, + tokenContractAddress = tokenContractAddress, + balance = balance, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index c1842bbec1..3887f44e6f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -15,10 +15,9 @@ import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher @@ -32,6 +31,7 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Binds import dagger.Module @@ -118,7 +118,7 @@ internal interface TangemPayDataModule { persistenceDataStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( moshi = moshi, - types = mapWithStringKeyTypes(), + types = mapWithStringKeyTypes(), defaultValue = emptyMap(), ), produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index fe1aa3f0bd..57cc801313 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -2,21 +2,23 @@ package com.tangem.data.pay.flow import arrow.core.Either import com.tangem.data.pay.store.PaymentAccountStatusesStore -import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "PaymentAccountStatusFetcher" @@ -29,147 +31,196 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : PaymentAccountStatusFetcher { + private val logger = TangemLogger.withTag(TAG) + override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = - eitherOn(dispatchers.default) { - Timber.tag(TAG).i("fetch: ${params.userWalletId.stringValue}") + Either.catchOn(dispatchers.default) { + val account = Account.Payment(userWalletId = params.userWalletId) + logger.i("fetch: ${params.userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") + logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") + logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") - return@eitherOn paymentAccountStatusesStore.store( + return@catchOn paymentAccountStatusesStore.store( userWalletId = params.userWalletId, - status = PaymentAccountStatus.Error.ExposedDevice, + status = AccountStatus.Payment( + account = account, + value = PaymentAccountStatusValue.Error.ExposedDevice, + ), ) } val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) .fold( ifLeft = { error -> - Timber.tag(TAG).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + logger.e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") when (error) { - is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated - else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + else -> PaymentAccountStatusValue.Error.Unavailable } }, ifRight = { hasTangemPay -> - proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) + proceedHasTangemPayResult(account = account, hasTangemPay = hasTangemPay) }, ) - Timber.tag(TAG).i("invoke status ${params.userWalletId}: $status") - paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) + logger.i("invoke status ${params.userWalletId}: $status") + paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = AccountStatus.Payment(account = account, value = status), + ) + }.onLeft { + paymentAccountStatusesStore.updateStatusSource( + userWalletId = params.userWalletId, + source = StatusSource.ONLY_CACHE, + ) } private suspend fun proceedHasTangemPayResult( - userWalletId: UserWalletId, + account: Account.Payment, hasTangemPay: Boolean, - ): PaymentAccountStatus { - Timber.tag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + ): PaymentAccountStatusValue { + logger.i("proceedHasTangemPayResult for ${account.userWalletId} hasTangemPay: $hasTangemPay") return if (hasTangemPay) { - fetchTangemPayAccountStatus(userWalletId = userWalletId) + fetchTangemPayAccountStatus(account) } else { - PaymentAccountStatus.NotCreated + PaymentAccountStatusValue.NotCreated } } - private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { - val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) - if (prevResult == null || prevResult is PaymentAccountStatus.Error) { - paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) + private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue { + val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId) + if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error) { + paymentAccountStatusesStore.store( + userWalletId = account.userWalletId, + status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading), + ) } - return proceedWithOrderId(userWalletId = userWalletId) + return proceedWithOrderId(account = account) } - private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { - return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { - PaymentAccountStatus.Error.NotSynced + private suspend fun proceedWithOrderId(account: Account.Payment): PaymentAccountStatusValue { + return if (!onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)) { + PaymentAccountStatusValue.Error.NotSynced } else { - val orderId = onboardingRepository.getOrderId(userWalletId) + val orderId = onboardingRepository.getOrderId(account.userWalletId) if (orderId != null) { - proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + proceedWithOrderId(account = account, orderId = orderId) } else { - proceedWithoutOrder(userWalletId = userWalletId) + proceedWithoutOrder(account = account) } } } - private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { - return onboardingRepository.getCustomerInfo(userWalletId).fold( + private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue { + return onboardingRepository.getCustomerInfo(account.userWalletId).fold( ifLeft = { error -> - Timber.tag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + logger.e("proceedWithoutOrder ${account.userWalletId} error: $error") error.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> - Timber.tag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") val status = customerInfo.mapToPaymentAccountStatus() - if (customerInfo.productInstance == null) { - onboardingRepository.createOrder(userWalletId) - .onLeft { Timber.tag(TAG).e("createOrder failed: $it") } + if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { + // If order id wasn't saved -> start order creation and get customer info + onboardingRepository.createOrder(account.userWalletId) + .onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") } } status }, ) } - private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { - return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( + private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue { + return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold( ifLeft = { error -> - Timber.tag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error") error.mapToPaymentAccountStatus() }, ifRight = { orderData -> - Timber.tag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + logger.i("proceedWithOrderId $account.userWalletId: $orderId status: ${orderData.status}") when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, OrderStatus.PROCESSING, - -> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) OrderStatus.CANCELED -> { - PaymentAccountStatus.Error.CardIssueFailed + PaymentAccountStatusValue.Error.CardIssueFailed(customerId = orderData.customerId) } OrderStatus.COMPLETED -> { // Order was completed -> clear order id and get customer info - onboardingRepository.clearOrderId(userWalletId) - onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + onboardingRepository.clearOrderId(account.userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) .fold( ifLeft = { it.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, ) } - OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable } }, ) } - private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { - PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) - } else if (cardInfo != null && productInstance != null) { - PaymentAccountStatus.Loaded( + PaymentAccountStatusValue.UnderReview( source = StatusSource.ACTUAL, - cardId = productInstance.cardId, - lastFourDigits = cardInfo.lastFourDigits, - balance = cardInfo.balance, - currencyCode = cardInfo.currencyCode, - depositAddress = cardInfo.depositAddress, - isPinSet = cardInfo.isPinSet, + kycStatus = kycStatus, + customerId = requireNotNull(customerId) { "CustomerId must not be null" }, + ) + } else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) { + convertToContentState( + productInstance = productInstance, + cardInfo = cardInfo, + customerId = requireNotNull(customerId) { "CustomerId must not be null" }, ) } else { - PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) } } - private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { + private fun convertToContentState( + productInstance: CustomerInfo.ProductInstance, + cardInfo: CustomerInfo.CardInfo, + customerId: String, + ): PaymentAccountStatusValue { + return when (productInstance.frozenState) { + TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked( + source = StatusSource.ACTUAL, + customerId = customerId, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + fiatBalance = cardInfo.fiatBalance, + cryptoBalance = cardInfo.cryptoBalance, + ) + else -> PaymentAccountStatusValue.Loaded( + source = StatusSource.ACTUAL, + customerId = customerId, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + fiatBalance = cardInfo.fiatBalance, + cryptoBalance = cardInfo.cryptoBalance, + ) + } + } + + private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatusValue { return when (this) { - is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced - is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated - else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + else -> PaymentAccountStatusValue.Error.Unavailable } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt index cb021c1ee8..c2c53ae7cd 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -4,8 +4,9 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.domain.core.flow.FlowProducerTools -import com.tangem.domain.models.StatusSource -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -21,12 +22,15 @@ internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( private val paymentAccountStatusesStore: PaymentAccountStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : PaymentAccountStatusProducer { - override val fallback: Option - get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some() - override fun produce(): Flow { + private val account = Account.Payment(userWalletId = params.userWalletId) + + override val fallback: Option + get() = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Error.Unavailable).some() + + override fun produce(): Flow { return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) - .onEmpty { emit(value = PaymentAccountStatus.NotCreated) } + .onEmpty { emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) } .flowOn(dispatchers.default) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 8e20d83b04..6880483883 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.TangemPayEligibilityType +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -139,6 +140,7 @@ internal class DefaultOnboardingRepository @Inject constructor( ?: error("no userWallet found") } + @Suppress("ComplexCondition") private suspend fun getCustomerInfo( userWalletId: UserWalletId, response: CustomerMeResponse.Result?, @@ -148,14 +150,26 @@ internal class DefaultOnboardingRepository @Inject constructor( val card = response?.card val fiatBalance = response?.balance?.fiat + val cryptoBalance = response?.balance?.crypto val paymentAccount = response?.paymentAccount - val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) { + val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) { CardInfo( lastFourDigits = card.cardNumberEnd, balance = fiatBalance.availableBalance, currencyCode = fiatBalance.currency, depositAddress = response.depositAddress, isPinSet = response.card?.isPinSet == true, + fiatBalance = PaymentAccountStatusValue.FiatBalance( + availableBalance = fiatBalance.availableBalance, + currency = fiatBalance.currency, + ), + cryptoBalance = PaymentAccountStatusValue.CryptoBalance( + id = cryptoBalance.id, + chainId = cryptoBalance.chainId.toLong(), + depositAddress = cryptoBalance.depositAddress.orEmpty(), + tokenContractAddress = cryptoBalance.tokenContractAddress, + balance = cryptoBalance.balance, + ), ) } else { null @@ -167,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId) + ProductInstance(id = instance.id, cardId = instance.cardId, frozenState = cardFrozenState) } return CustomerInfo( customerId = response?.id, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 0c0b28df99..2f05b72de7 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -19,18 +19,21 @@ import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.visa.model.TangemPayCardFrozenState -import kotlinx.coroutines.* +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -296,7 +299,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( cardFrozenStateStore.store(cardId, finalState) } }.onLeft { error -> - Timber.e("error ${error.errorCode}") + TangemLogger.e("error ${error.errorCode}") // stop retrying after 3 errors if (retryCount > MAX_POLLING_RETRIES) { pollingJobs.remove(key = orderId) @@ -305,7 +308,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( retryCount++ } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) storePollingMutex.withLock { pollingJobs.remove(orderId) } @@ -338,7 +341,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } private fun catchException(throwable: Throwable): Either { - Timber.tag(TAG).e(throwable) + TangemLogger.withTag(TAG).e("Error", throwable) return errorConverter.convert(throwable).left() } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index 555c6c8e32..74512256e4 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -24,13 +24,13 @@ import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addHexPrefix +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode import java.util.Currency @@ -176,14 +176,14 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( } } if (attemptCount >= MAX_POLLING_ATTEMPTS) { - Timber.tag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") + TangemLogger.withTag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) pollingMutex.withLock { pollingJobs.remove(key) } } } catch (exception: CancellationException) { throw exception } catch (exception: Exception) { - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) pollingMutex.withLock { pollingJobs.remove(key) } } @@ -240,7 +240,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( } catch (exception: CancellationException) { throw exception } catch (exception: Exception) { - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) } } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 3e83c4ff0d..c29153ce52 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -17,10 +17,10 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.getAuthHeader import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.UUID import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -69,7 +69,7 @@ internal class TangemPayRequestPerformer @Inject constructor( } }, catch = { errorConverter.convert(it).left() }, - ).onLeft { visaApiError -> Timber.tag(TAG).e(visaApiError.toString()) } + ).onLeft { visaApiError -> TangemLogger.withTag(TAG).e(visaApiError.toString()) } } suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String { @@ -129,7 +129,7 @@ internal class TangemPayRequestPerformer @Inject constructor( idempotencyKey = UUID.randomUUID().toString(), ) }.mapLeft { error -> - Timber.tag(TAG).e("Can not refresh auth tokens: $error") + TangemLogger.withTag(TAG).e("Can not refresh auth tokens: $error") if (error is VisaApiError.ServerUnavailable) error else VisaApiError.RefreshTokenExpired }.onRight { tokens -> tangemPayStorage.storeAuthTokens(customerWalletAddress = customerWalletAddress, tokens = tokens) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index 4eec265d04..ec8a48d880 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -1,21 +1,24 @@ package com.tangem.data.pay.store import androidx.datastore.core.DataStore -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM -import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch -import timber.log.Timber -internal typealias WalletIdWithPaymentStatus = Map -internal typealias WalletIdWithPaymentStatusDM = Map +internal typealias WalletIdWithPaymentStatus = Map +internal typealias WalletIdWithPaymentStatusDM = Map /** * Store for payment account statuses with dual storage (runtime + persistence). @@ -26,7 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map, private val persistenceDataStore: DataStore, - private val scope: AppCoroutineScope, + scope: AppCoroutineScope, ) { init { @@ -34,28 +37,40 @@ internal class PaymentAccountStatusesStore( try { val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch runtimeStore.store( - value = cachedStatuses.mapValues { (_, statusDM) -> - PaymentAccountStatusDMConverter.convertBack(statusDM) + value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) -> + val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId)) + val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM) + AccountStatus.Payment(account = account, value = statusValue) }, ) } catch (e: Exception) { - Timber.e(e, "Error while loading cached payment account statuses") + TangemLogger.e("Error while loading cached payment account statuses", e) } } } - fun get(userWalletId: UserWalletId): Flow { + fun get(userWalletId: UserWalletId): Flow { return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } } - suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Payment? { return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) } - suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) { + runtimeStore.update(emptyMap()) { stored -> + stored.toMutableMap().apply { + val paymentAccountStatus = this[userWalletId.stringValue] ?: return@update stored + val newValue = paymentAccountStatus.copy(value = paymentAccountStatus.value.copySealed(source = source)) + put(key = userWalletId.stringValue, value = newValue) + } + } + } + + suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) { coroutineScope { launch { storeInRuntime(userWalletId = userWalletId, status = status) } - launch { storeInPersistence(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status.value) } } } @@ -63,7 +78,7 @@ internal class PaymentAccountStatusesStore( return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) } - private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Payment) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { put(key = userWalletId.stringValue, value = status) @@ -71,8 +86,8 @@ internal class PaymentAccountStatusesStore( } } - private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { - val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) { + val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return persistenceDataStore.updateData { storedStatuses -> storedStatuses.toMutableMap().apply { put(key = userWalletId.stringValue, value = statusDM) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt index be1d69448d..c75c111c9d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt @@ -7,9 +7,9 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isPositive import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import org.joda.time.DateTime import org.joda.time.DateTimeZone -import timber.log.Timber import java.util.Currency internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : @@ -26,7 +26,7 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : ?: value.fee?.let { convertFee(id = value.id, fee = it) } ?: value.collateral?.let { convertCollateral(id = value.id, collateral = it) } ?: run { - Timber.wtf("unknown type of transaction: $value") + TangemLogger.e("unknown type of transaction: $value") null } } @@ -82,7 +82,7 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : collateral: TangemPayTxHistoryResponse.Collateral, ): TangemPayTxHistoryItem.Collateral? { val date = collateral.postedAt ?: return run { - Timber.e("Collateral transaction postedAt is null: $collateral") + TangemLogger.e("Collateral transaction postedAt is null: $collateral") return@run null } return TangemPayTxHistoryItem.Collateral( diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 07ebc8a190..df5b49ac6c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -7,10 +7,10 @@ import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext -import timber.log.Timber internal class VisaTxHistoryPagingSource( params: Params, @@ -56,7 +56,7 @@ internal class VisaTxHistoryPagingSource( LoadResult.Page(items, prevOffset, nextOffset) } catch (e: Throwable) { - Timber.e(e, "Unable to load the transaction history for the requested offset: $offsetToLoad") + TangemLogger.e("Unable to load the transaction history for the requested offset: $offsetToLoad", e) LoadResult.Error(e) } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index b35c96e84d..13522ffe02 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -9,10 +9,10 @@ import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager -import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WC_TAG +import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultWcInitializeUseCase( private val application: Application, @@ -48,7 +48,7 @@ internal class DefaultWcInitializeUseCase( application = application, metaData = appMetaData, ) { error -> - Timber.tag(WC_TAG).e("Error while initializing client: $error") + TangemLogger.withTag(WC_TAG).e("Error while initializing client: $error") } WalletKit.initialize( @@ -57,10 +57,10 @@ internal class DefaultWcInitializeUseCase( val walletDelegate = defineWalletDelegate() WalletKit.setWalletDelegate(walletDelegate) wcSdkObservers.forEach { it.onWcSdkInit() } - Timber.tag(WC_TAG).i("onWcSdkInit") + TangemLogger.withTag(WC_TAG).i("onWcSdkInit") }, onError = { error -> - Timber.tag(WC_TAG).e("Error while initializing Web3Wallet: $error") + TangemLogger.withTag(WC_TAG).e("Error while initializing Web3Wallet: $error") }, ) } @@ -70,32 +70,32 @@ internal class DefaultWcInitializeUseCase( get() = super.onSessionAuthenticate override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { - Timber.tag(WC_TAG).i("sdk callback onConnectionStateChange isAvailable=${state.isAvailable}") + TangemLogger.withTag(WC_TAG).i("sdk callback onConnectionStateChange isAvailable=${state.isAvailable}") wcSdkObservers.forEach { it.onConnectionStateChange(state) } } override fun onError(error: Wallet.Model.Error) { - Timber.tag(WC_TAG).e(error.throwable, "sdk callback onError") + TangemLogger.withTag(WC_TAG).e("sdk callback onError", error.throwable) wcSdkObservers.forEach { it.onError(error) } } override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) { - Timber.tag(WC_TAG).i("sdk callback onProposalExpired $proposal") + TangemLogger.withTag(WC_TAG).i("sdk callback onProposalExpired $proposal") wcSdkObservers.forEach { it.onProposalExpired(proposal) } } override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) { - Timber.tag(WC_TAG).i("sdk callback onRequestExpired $request") + TangemLogger.withTag(WC_TAG).i("sdk callback onRequestExpired $request") wcSdkObservers.forEach { it.onRequestExpired(request) } } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - Timber.tag(WC_TAG).i("sdk callback onSessionDelete $sessionDelete") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionDelete $sessionDelete") wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) } } override fun onSessionExtend(session: Wallet.Model.Session) { - Timber.tag(WC_TAG).i("sdk callback onSessionExtend $session") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionExtend $session") wcSdkObservers.forEach { it.onSessionExtend(session) } } @@ -103,7 +103,7 @@ internal class DefaultWcInitializeUseCase( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, ) { - Timber.tag(WC_TAG).i("sdk callback onSessionProposal $sessionProposal") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionProposal $sessionProposal") wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) } } @@ -111,17 +111,17 @@ internal class DefaultWcInitializeUseCase( sessionRequest: Wallet.Model.SessionRequest, verifyContext: Wallet.Model.VerifyContext, ) { - Timber.tag(WC_TAG).i("sdk callback onSessionRequest $sessionRequest") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionRequest $sessionRequest") wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) } } override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { - Timber.tag(WC_TAG).i("sdk callback onSessionSettleResponse $settleSessionResponse") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionSettleResponse $settleSessionResponse") wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) } } override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { - Timber.tag(WC_TAG).i("sdk callback onSessionUpdateResponse $sessionUpdateResponse") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionUpdateResponse $sessionUpdateResponse") wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) } } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt index 312d752068..6347e2f390 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt @@ -1,17 +1,17 @@ package com.tangem.data.walletconnect.network.solana import com.tangem.blockchain.extensions.decodeBase58 -import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.blockchain.extensions.encodeBase64NoWrap +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.utils.converter.Converter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class SolanaBlockAidAddressConverter @Inject constructor() : Converter { override fun convert(value: String): String? { return value.decodeBase58()?.encodeBase64NoWrap() ?: run { - Timber.tag(WC_TAG).e("Error while converting Solana transaction account address") + TangemLogger.withTag(WC_TAG).e("Error while converting Solana transaction account address") null } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt index c0d665b854..157cfaf912 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt @@ -22,13 +22,13 @@ import com.tangem.domain.walletconnect.usecase.method.SignRequirements import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import okio.ByteString.Companion.decodeBase64 -import timber.log.Timber @Suppress("LongParameterList") internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( @@ -57,13 +57,13 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( val formattedHash = getFormattedHash(hash) // uses for flow sendLargeSolanaTransaction if (context.session.wallet is UserWallet.Cold && isLargeHash(formattedHash)) { // workaround for large transactions that cannot be signed directly by card - Timber.w("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes") + TangemLogger.w("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes") sendLargeSolanaTransactionUseCase(context.session.wallet as UserWallet.Cold, context.network, formattedHash) .fold( - ifLeft = { + ifLeft = { error -> analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Failed)) - Timber.e(it.toString()) - emit(state.toResult(parseSendError(it).left())) + TangemLogger.e(error.toString()) + emit(state.toResult(parseSendError(error).left())) }, ifRight = { analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Success)) @@ -115,7 +115,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( return try { SolanaTransactionHelper.removeSignaturesPlaceholders(hash) } catch (e: Exception) { - Timber.e("Failed to format the hash: ${e.message}") + TangemLogger.e("Failed to format the hash: ${e.message}") hash } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 78d58bd321..02ce4d738f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -16,6 +16,7 @@ import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -24,7 +25,6 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime import org.joda.time.Duration -import timber.log.Timber import java.net.URI @Suppress("LongParameterList") @@ -42,7 +42,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( @Suppress("LongMethod") override operator fun invoke(): Flow { return flow { - Timber.tag(WC_TAG).i("start pair flow $pairRequest") + TangemLogger.withTag(WC_TAG).i("start pair flow $pairRequest") analytics.send( WcAnalyticEvents.NewPairInitiated( source = pairRequest.source, @@ -52,22 +52,22 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( emit(WcPairState.Loading) val pairResult = sdkDelegate.pair(pairRequest.uri) - .onLeft { - Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest") + .onLeft { error -> + TangemLogger.withTag(WC_TAG).e("Failed to call pair $pairRequest", error) analytics.send( WcAnalyticEvents.PairFailed( - errorCode = it.code, - errorMessage = it.message, + errorCode = error.code, + errorMessage = error.message, ), ) - emit(WcPairState.Error(it)) + emit(WcPairState.Error(error)) } .getOrNull() ?: return@flow val (sdkSessionProposal, sdkVerifyContext) = pairResult // check unsupported dApps, just local constant for now, finish if unsupported if (UnsupportedDApps.list.any { sdkSessionProposal.url.contains(it, ignoreCase = true) }) { - Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") + TangemLogger.withTag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") val error = WcPairState.Error(WcPairError.UnsupportedDApp(sdkSessionProposal.name)) emit(error) return@flow @@ -93,7 +93,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( emit(proposalState) // wait first terminal action and continue WC pair flow - Timber.tag(WC_TAG).i("pair wait terminal action ${sdkSessionProposal.name}") + TangemLogger.withTag(WC_TAG).i("pair wait terminal action ${sdkSessionProposal.name}") val terminalAction = onCallTerminalAction.receiveAsFlow().first() val sessionForApprove: WcSessionApprove? = when (terminalAction) { is TerminalAction.Approve -> terminalAction.sessionForApprove @@ -139,30 +139,30 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ), ) proposalState.dAppSession.dAppMetaData - }.onLeft { + }.onLeft { error -> analytics.send( WcAnalyticEvents.DAppConnectionFailed( - errorCode = it.code, - errorMessage = it.message, + errorCode = error.code, + errorMessage = error.message, ), ) sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) - Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") + TangemLogger.withTag(WC_TAG).e("Failed to approve session ${sdkSessionProposal.name}", error) } emit(WcPairState.Approving.Result(sessionForApprove, either)) } - .catch { - val pairError: WcPairError = when (it) { - is TimeoutCancellationException -> WcPairError.TimeoutException(it.message.orEmpty()) - else -> WcPairError.Unknown(it.message.orEmpty()) + .catch { throwable -> + val pairError: WcPairError = when (throwable) { + is TimeoutCancellationException -> WcPairError.TimeoutException(throwable.message.orEmpty()) + else -> WcPairError.Unknown(throwable.message.orEmpty()) } emit(WcPairState.Error(pairError)) } - .onCompletion { - if (it != null) { - Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") + .onCompletion { throwable -> + if (throwable != null) { + TangemLogger.withTag(WC_TAG).e("Completed with error $pairRequest", throwable) } else { - Timber.tag(WC_TAG).i("Completed successfully $pairRequest") + TangemLogger.withTag(WC_TAG).i("Completed successfully $pairRequest") } } } @@ -191,7 +191,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ) sdkDelegate.approve(pendingSessionForSave, sessionApprove) } catch (e: Throwable) { - Timber.tag(WC_TAG).e(e, "Failed to sdk approve session $pairRequest") + TangemLogger.withTag(WC_TAG).e("Failed to sdk approve session $pairRequest", e) WcPairError.ApprovalFailed(e.message.orEmpty()).left() } @@ -203,8 +203,8 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( val verificationInfo = when { verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE verifyContext.isScam == true -> CheckDAppResult.UNSAFE - else -> blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { - Timber.tag(WC_TAG).e(it, "Failed to verify DApp ${sessionProposal.name}") + else -> blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { error -> + TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error) CheckDAppResult.FAILED_TO_VERIFY } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 926afee1d3..a747091d4a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -7,17 +7,17 @@ import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.data.walletconnect.utils.getDappOriginUrl +import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* -import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds @@ -91,7 +91,7 @@ internal class WcPairSdkDelegate( } fun rejectSession(proposerPublicKey: String) { - Timber.tag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey") + TangemLogger.withTag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey") WalletKit.rejectSession( params = Wallet.Params.SessionReject( proposerPublicKey = proposerPublicKey, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index b2393fe07f..130530c326 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -9,10 +9,10 @@ import com.tangem.data.walletconnect.utils.getDappOriginUrl import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.model.WcMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime -import timber.log.Timber internal class DefaultWcRequestService( private val requestConverters: Set, @@ -35,10 +35,10 @@ internal class DefaultWcRequestService( sessionRequest = sessionRequest, ), ) - Timber.tag(WC_TAG).i("handle request $sr") + TangemLogger.withTag(WC_TAG).i("handle request $sr") val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) } ?: WcMethodName.Unsupported(sr.request.method) - Timber.tag(WC_TAG).i("handle request name $name") + TangemLogger.withTag(WC_TAG).i("handle request name $name") if (name is WcMethodName.Unsupported) { respondService.rejectRequestNonBlock(sr) if (name.raw.startsWith("wallet_")) return @@ -56,7 +56,7 @@ internal class DefaultWcRequestService( val noHaveCached = cachedRequest.none { (_, hashParams) -> hash == hashParams } if (!noHaveCached) { - Timber.tag(WC_TAG).i("filter request $request") + TangemLogger.withTag(WC_TAG).i("filter request $request") } return noHaveCached } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt index 9745a0f84f..41caf2afe1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt @@ -13,7 +13,7 @@ import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class DefaultWcRequestUseCaseFactory @Inject constructor( @@ -32,9 +32,9 @@ internal class DefaultWcRequestUseCaseFactory @Inject constructor( ?: HandleMethodError.UnknownError("Failed to create WcUseCase").left() val result = useCase.fold( - ifLeft = { - Timber.tag(WC_TAG).e("$it") - it.left() + ifLeft = { methodError -> + TangemLogger.withTag(WC_TAG).e("$methodError") + methodError.left() }, ifRight = { (it as? T)?.right() ?: HandleMethodError.Unsupported(WcMethod.Unsupported(request)).left() }, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index ca0bc550dd..fdb3bf7bb1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -10,11 +10,11 @@ import com.tangem.common.extensions.toHexString import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.suspendCancellableCoroutine import org.joda.time.Duration -import timber.log.Timber import kotlin.coroutines.resume internal class DefaultWcRespondService : WcRespondService { @@ -36,21 +36,21 @@ internal class DefaultWcRespondService : WcRespondService { result = response, ), ), - onSuccess = { + onSuccess = { requestResponse -> if (continuation.isCompleted) return@respondSessionRequest - val result = when (val response = it.jsonRpcResponse) { + val result = when (val response = requestResponse.jsonRpcResponse) { is Wallet.Model.JsonRpcResponse.JsonRpcError -> { - Timber.tag(WC_TAG).e("Failed respond $response for request $request") + TangemLogger.withTag(WC_TAG).e("Failed respond $response for request $request") WcRequestError.WcRespondError( code = response.code, message = response.message, ).left() } is Wallet.Model.JsonRpcResponse.JsonRpcResult -> { - Timber.tag(WC_TAG).i("Successful respond $response for request $request") + TangemLogger.withTag(WC_TAG).i("Successful respond $response for request $request") if (response.result == null) { - Timber.tag(WC_TAG).e( - "Response result is null, but it should be String. Casted to empty", + TangemLogger.withTag(WC_TAG).e( + "Response result is null, but requestResponse should be String. Casted to empty", ) } (response.result ?: "").right() @@ -58,16 +58,16 @@ internal class DefaultWcRespondService : WcRespondService { } continuation.resume(result) }, - onError = { + onError = { error -> if (continuation.isCompleted) return@respondSessionRequest - Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") - continuation.resume(WcRequestError.UnknownError(it.throwable).left()) + TangemLogger.withTag(WC_TAG).e("Failed respond for request $request", error.throwable) + continuation.resume(WcRequestError.UnknownError(error.throwable).left()) }, ) } override fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String) { - Timber.tag(WC_TAG).i("reject request $request") + TangemLogger.withTag(WC_TAG).i("reject request $request") removeCachedRequest(request) WalletKit.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index d27195854e..c6fed93088 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -23,13 +23,13 @@ import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import timber.log.Timber import kotlin.coroutines.resume @Suppress("LongParameterList") @@ -91,7 +91,7 @@ internal class DefaultWcSessionsManager( } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - Timber.i("onSessionDelete: $sessionDelete") + TangemLogger.i("onSessionDelete: $sessionDelete") onSessionDelete.trySend(sessionDelete) } @@ -150,7 +150,7 @@ internal class DefaultWcSessionsManager( val haveSomeUnknownSdkSessions = unknownSdkSessions.isNotEmpty() if (haveSomeUnknown) { - Timber.tag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions") + TangemLogger.withTag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions") store.removeSessions(unknownStoredSessions.toSet()) } @@ -162,7 +162,7 @@ internal class DefaultWcSessionsManager( val haveEmptyDto = emptyNetworksDto.isNotEmpty() if (haveEmptyDto) { - Timber.tag(WC_TAG).i("remove sessions without networks $emptyNetworksDto") + TangemLogger.withTag(WC_TAG).i("remove sessions without networks $emptyNetworksDto") store.removeSessions(emptyNetworksDto) } if (haveEmptySessions) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index 8bc9a0c492..f0d71c240c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -11,8 +11,8 @@ import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.flow -import timber.log.Timber import javax.inject.Inject internal class BlockAidVerificationDelegate @Inject constructor( @@ -72,9 +72,9 @@ internal class BlockAidVerificationDelegate @Inject constructor( params = params, ), ).fold( - ifLeft = { - Timber.e("Failed to verify transaction: ${it.localizedMessage}") - emit(Lce.Error(it)) + ifLeft = { throwable -> + TangemLogger.e("Failed to verify transaction: ${throwable.localizedMessage}") + emit(Lce.Error(throwable)) }, ifRight = { emit(Lce.Content(it)) diff --git a/data/wallet-manager/build.gradle.kts b/data/wallet-manager/build.gradle.kts index e703847de0..ace6ee7b8a 100644 --- a/data/wallet-manager/build.gradle.kts +++ b/data/wallet-manager/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(deps.androidx.datastore) implementation(deps.arrow.core) - implementation(deps.timber) /** Testing libraries */ testRuntimeOnly(deps.test.junit5.engine) diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 5791e4e9e0..68f31b8849 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -49,11 +49,11 @@ import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.utils.SdkPageConverter import com.tangem.domain.wallets.extension.hasDerivation import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import java.math.BigDecimal import java.util.EnumSet import java.util.concurrent.ConcurrentHashMap @@ -178,13 +178,13 @@ internal class DefaultWalletManagersFacade @Inject constructor( if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath) ) { - Timber.w("Derivation missed for: $blockchain") + TangemLogger.w("Derivation missed for: $blockchain") return UpdateWalletManagerResult.MissedDerivation } val walletManager = getOrCreateWalletManager(userWalletId, blockchain, derivationPath) if (walletManager == null || blockchain == Blockchain.Unknown) { - Timber.w("Unable to get a wallet manager for blockchain: $blockchain") + TangemLogger.w("Unable to get a wallet manager for blockchain: $blockchain") return UpdateWalletManagerResult.Unreachable() } @@ -285,7 +285,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( val gaslessFeeAddresses = try { gaslessTransactionRepository.getGaslessFeeAddresses() } catch (error: Throwable) { - Timber.e(error, "Failed to load gasless fee addresses; falling back to empty set") + TangemLogger.e("Failed to load gasless fee addresses; falling back to empty set", error) emptySet() } return when (itemsResult) { @@ -311,7 +311,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( extraTokens: Set, ): UpdateWalletManagerResult { if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath)) { - Timber.w("Derivation missed for: $blockchain") + TangemLogger.w("Derivation missed for: $blockchain") return UpdateWalletManagerResult.MissedDerivation } @@ -321,7 +321,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( derivationPath = derivationPath, ) if (walletManager == null || blockchain == Blockchain.Unknown) { - Timber.w("Unable to create or find a wallet manager for blockchain: $blockchain") + TangemLogger.w("Unable to create or find a wallet manager for blockchain: $blockchain") return UpdateWalletManagerResult.Unreachable() } @@ -360,7 +360,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( amountToCreateAccount = e.amountToCreateAccount, ) } catch (e: Throwable) { - Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + TangemLogger.w("Unable to update a wallet manager for: ${walletManager.wallet.blockchain}", e) resultFactory.getUnreachableResult(walletManager) } @@ -376,7 +376,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( amountToCreateAccount = e.amountToCreateAccount, ) } catch (e: Throwable) { - Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + TangemLogger.w("Unable to update a wallet manager for: ${walletManager.wallet.blockchain}", e) resultFactory.getUnreachableResult(walletManager) } @@ -584,7 +584,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) if (walletManager == null) { - Timber.e("Unable to get a wallet manager for blockchain: ${currency.network.id}") + TangemLogger.e("Unable to get a wallet manager for blockchain: ${currency.network.id}") return emptyList() } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt index a8d65edb01..c1ab2fb300 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt @@ -8,7 +8,7 @@ import com.tangem.data.walletmanager.utils.SdkAddressToAddressConverter import com.tangem.data.walletmanager.utils.TransactionDataToTxHistoryItemConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** Factory for creating [UpdateWalletManagerResult] */ @@ -68,7 +68,7 @@ internal class UpdateWalletManagerResultFactory { val amount = amountToCreateAccount ?: blockchain.amountToCreateAccount(walletManager, firstWalletToken) return if (amount == null) { - Timber.w("Unable to get required amount to create account for: $blockchain") + TangemLogger.w("Unable to get required amount to create account for: $blockchain") UpdateWalletManagerResult.Unreachable( selectedAddress = wallet.address, addresses = getAvailableAddresses(wallet.addresses), @@ -142,7 +142,7 @@ internal class UpdateWalletManagerResultFactory { val value = amount.value if (value == null) { - Timber.w("Currency amount must not be null: ${amount.currencySymbol}") + TangemLogger.w("Currency amount must not be null: ${amount.currencySymbol}") } return value diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index edd6da8af5..e4b9650c5d 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,12 +7,12 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp -import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class WalletManagerFactory( private val blockchainSDKFactory: BlockchainSDKFactory, @@ -32,7 +32,7 @@ internal class WalletManagerFactory( derivationParams = derivationParams, ) } catch (e: Throwable) { - Timber.w(e, "Failed to create wallet manager for $blockchain") + TangemLogger.w("Failed to create wallet manager for $blockchain", e) null } } @@ -68,7 +68,7 @@ internal class WalletManagerFactory( ) } } catch (e: Throwable) { - Timber.w(e, "Failed to create wallet manager for $blockchain") + TangemLogger.w("Failed to create wallet manager for $blockchain", e) null } } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index 36016b121b..1247fce030 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -11,7 +11,7 @@ import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldS import com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Address import com.tangem.domain.models.network.TxInfo import com.tangem.utils.converter.Converter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** @@ -55,7 +55,7 @@ internal class TransactionDataToTxHistoryItemConverter( val value = amount.value if (value == null) { - Timber.w("Transaction amount must not be null: ${amount.currencySymbol}") + TangemLogger.w("Transaction amount must not be null: ${amount.currencySymbol}") } return when (feePaidCurrency) { diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 512c474032..19a6351a4c 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.retrofit) - implementation(deps.timber) /** tests */ testImplementation(projects.test.core) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt index e5fb0f7e81..22b82c3fb7 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt @@ -14,8 +14,8 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.repository.WalletsPromoRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber internal class DefaultWalletsPromoRepository( private val appPreferencesStore: AppPreferencesStore, @@ -47,7 +47,7 @@ internal class DefaultWalletsPromoRepository( if (savedBindingData != null) { bind(refcode = savedBindingData.refcode, campaign = savedBindingData.campaign) } else { - Timber.i("retryBindRefcodeWithWallets: Binding data isn't required") + TangemLogger.i("retryBindRefcodeWithWallets: Binding data isn't required") } } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt index e00bde14e5..f3305bfb5e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt @@ -20,8 +20,8 @@ import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject private typealias DerivedKeys = Map @@ -60,14 +60,14 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor( networks: List, ): UserWallet.Cold = withContext(dispatchers.io) { if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return@withContext userWallet } val derivations = MissedDerivationsFinder(userWallet) .findByNetworks(networks) .ifEmpty { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return@withContext userWallet } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt index 534b16cb36..c347bb1e54 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -16,8 +16,8 @@ import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject internal class DefaultDerivationsRepository @Inject constructor( @@ -29,7 +29,7 @@ internal class DefaultDerivationsRepository @Inject constructor( override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt index 8d0f7ec5d1..bcf3ef947c 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -19,8 +19,8 @@ import com.tangem.domain.wallets.usecase.BackendId import com.tangem.hot.sdk.model.DeriveWalletRequest import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject internal class DefaultHotMapDerivationsRepository @Inject constructor( @@ -62,7 +62,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( val derivations = MissedDerivationsFinder(userWallet) .findByNetworks(networks) .ifEmpty { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return@withContext userWallet } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt index 99e2bde6ae..3adc1bc3a8 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt @@ -10,10 +10,10 @@ import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.hot.sdk.model.DataToSign import com.tangem.operations.sign.SignData import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber class TangemHotWalletSigner @AssistedInject constructor( @Assisted private val userWallet: UserWallet.Hot, @@ -44,12 +44,12 @@ class TangemHotWalletSigner @AssistedInject constructor( ), ), ) - }.getOrElse { - Timber.e(it) - return if (it is TangemSdkError) { - CompletionResult.Failure(it) + }.getOrElse { throwable -> + TangemLogger.e("Error", throwable) + return if (throwable is TangemSdkError) { + CompletionResult.Failure(throwable) } else { - CompletionResult.Failure(TangemSdkError.ExceptionError(it)) + CompletionResult.Failure(TangemSdkError.ExceptionError(throwable)) } } @@ -77,12 +77,12 @@ class TangemHotWalletSigner @AssistedInject constructor( ) }, ) - }.getOrElse { - Timber.e(it) - return if (it is TangemSdkError) { - CompletionResult.Failure(it) + }.getOrElse { throwable -> + TangemLogger.e("Error", throwable) + return if (throwable is TangemSdkError) { + CompletionResult.Failure(throwable) } else { - CompletionResult.Failure(TangemSdkError.ExceptionError(it)) + CompletionResult.Failure(TangemSdkError.ExceptionError(throwable)) } } diff --git a/data/yield-supply/build.gradle.kts b/data/yield-supply/build.gradle.kts index cc7313cd67..d5edaba7cd 100644 --- a/data/yield-supply/build.gradle.kts +++ b/data/yield-supply/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other */ - implementation(deps.timber) /** tests */ testImplementation(projects.common.test) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt index c761bd1160..19f06ecd0f 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt @@ -18,8 +18,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import java.math.BigDecimal @Suppress("LargeClass") @@ -109,7 +109,7 @@ internal class DefaultYieldSupplyTransactionRepository( decimals = cryptoCurrency.decimals, ), ) - }.onFailure(Timber::e).getOrThrow() + }.onFailure { TangemLogger.e("Error", it) }.getOrThrow() } @Suppress("LongParameterList") @@ -202,7 +202,7 @@ internal class DefaultYieldSupplyTransactionRepository( derivationPath = cryptoCurrency.network.derivationPath.value, ) ?: error("Wallet manager not found") walletManager.calculateYieldModuleAddress() - }.onFailure(Timber::e).getOrThrow() + }.onFailure { TangemLogger.e("Error", it) }.getOrThrow() } override suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? = @@ -215,7 +215,7 @@ internal class DefaultYieldSupplyTransactionRepository( derivationPath = cryptoCurrency.network.derivationPath.value, ) ?: error("Wallet manager not found") walletManager.getYieldModuleAddress() - }.onFailure(Timber::e).getOrThrow() + }.onFailure { TangemLogger.e("Error", it) }.getOrThrow() } private suspend fun getYieldTokenStatus( @@ -249,7 +249,7 @@ internal class DefaultYieldSupplyTransactionRepository( isAllowedToSpend = isAllowedToSpend, effectiveProtocolBalance = protocolBalance, ) - }.onFailure(Timber::e).getOrNull() + }.onFailure { TangemLogger.e("Error", it) }.getOrNull() } private fun createDeployTransaction( diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index c413c7e46f..ab66eca0fd 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -108,16 +108,14 @@ data class AccountList private constructor( } fun flattenMapCurrencies(): Map = buildMap { - accounts.forEach { acc -> - val account = when (acc) { - is Account.CryptoPortfolio -> acc - is Account.Payment -> TODO("[REDACTED_JIRA]") + accounts + .filterIsInstance() + .forEach { account -> + account.cryptoCurrencies.forEach { currency -> + val key = account.accountId to currency.id + put(key, currency) + } } - account.cryptoCurrencies.forEach { currency -> - val key = account.accountId to currency.id - put(key, currency) - } - } } /** diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index d22d8549d9..20d40f33d4 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -28,18 +28,20 @@ dependencies { api(projects.domain.staking) api(projects.domain.tokens) api(projects.domain.tokens.models) + api(projects.domain.visa) api(projects.domain.walletManager) api(projects.domain.wallets) implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) + implementation(projects.core.utils) implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) - implementation(deps.timber) - implementation(deps.kermit) implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) // region DI implementation(deps.hilt.android) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt index e42901b839..1b49f91932 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt @@ -1,12 +1,12 @@ package com.tangem.domain.account.status.producer -import co.touchlab.kermit.Logger import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -55,7 +55,7 @@ class DefaultFlowProducerTools @Inject constructor( private fun logError(cause: Throwable, flowProducerName: String, attempt: Long) { val tag = "FlowProducerRetryWhen" - Logger.withTag(tag) + TangemLogger.withTag(tag) .e("flowProducerName $flowProducerName attempt $attempt", cause) val event = ExceptionAnalyticsEvent( diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 5250d458de..1c341528d0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -3,6 +3,7 @@ package com.tangem.domain.account.status.producer import arrow.core.Option import arrow.core.none import arrow.core.toOption +import com.tangem.common.card.FirmwareVersion import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.domain.account.models.AccountCurrencyId import com.tangem.domain.account.models.AccountList @@ -33,6 +34,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.quotes.multi.MultiQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceProducer @@ -42,6 +44,7 @@ import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.domain.tokens.operations.PriceChangeCalculator import com.tangem.domain.tokens.operations.TokenListFactory import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -69,6 +72,7 @@ import java.math.BigDecimal * [REDACTED_AUTHOR] */ +// TODO: Move to :data:account:status [REDACTED_JIRA] @Suppress("LongParameterList") @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @@ -76,6 +80,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo override val flowProducerTools: FlowProducerTools, private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, private val networkStatusSupplier: MultiNetworkStatusSupplier, @@ -116,31 +121,52 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flattenCurrency = flattenCurrency, ) - combine( + if (userWallet.isPaymentAccountSupported()) { + combineWithPaymentAccount( + accountListFlow = accountListFlow, + cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, + paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId), + ) + } else { + combineWithoutPaymentAccount( + accountListFlow = accountListFlow, + cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, + ) + } + .collect { accountStatusList -> channel.send(accountStatusList) } + } + + private fun combineWithPaymentAccount( + accountListFlow: StateFlow, + cryptoCurrencyStatusFlow: Flow>, + paymentAccountStatusFlow: Flow, + ): Flow { + return combine( flow = accountListFlow, flow2 = cryptoCurrencyStatusFlow, - transform = { accountList, currencyStatusMap -> - val accountStatuses: List = accountList.accounts.map { acc -> - val account: Account.CryptoPortfolio = when (acc) { - is Account.CryptoPortfolio -> acc - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - if (account.cryptoCurrencies.isEmpty()) { - account.toEmptyAccountStatus() - } else { - val statuses: List = account.cryptoCurrencies.map { currency -> - val acId = account.accountId to currency.id - currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + flow3 = paymentAccountStatusFlow, + transform = { accountList, currencyStatusMap, paymentAccountStatus -> + val accountStatuses = accountList.accounts.map { account -> + when (account) { + is Account.Payment -> paymentAccountStatus + is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) { + account.toEmptyAccountStatus() + } else { + val statuses: List = + account.cryptoCurrencies.map { currency -> + val acId = account.accountId to currency.id + currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + } + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), + ) } - AccountStatus.CryptoPortfolio( - account = account, - tokenList = TokenListFactory.create( - statuses = statuses, - groupType = accountList.groupType, - sortType = accountList.sortType, - ), - priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), - ) } } val balances = accountStatuses.flattenTotalFiatBalance() @@ -156,7 +182,58 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo ) }, ) - .collect { accountStatusList -> channel.send(accountStatusList) } + } + + private fun combineWithoutPaymentAccount( + accountListFlow: StateFlow, + cryptoCurrencyStatusFlow: Flow>, + ): Flow { + return combine( + flow = accountListFlow, + flow2 = cryptoCurrencyStatusFlow, + transform = { accountList, currencyStatusMap -> + val accountStatuses = accountList.accounts + .filterIsInstance() + .map { account -> + when (account) { + is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) { + account.toEmptyAccountStatus() + } else { + val statuses: List = + account.cryptoCurrencies.map { currency -> + val acId = account.accountId to currency.id + currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + } + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), + ) + } + } + } + val balances = accountStatuses.flattenTotalFiatBalance() + + AccountStatusList( + userWalletId = accountList.userWalletId, + accountStatuses = accountStatuses, + totalAccounts = accountList.totalAccounts, + totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), + totalArchivedAccounts = accountList.totalArchivedAccounts, + sortType = accountList.sortType, + groupType = accountList.groupType, + ) + }, + ) + } + + private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) { + is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } private fun ProducerScope.flattenCurrencyStatusFlow( @@ -278,7 +355,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo return map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.totalFiatBalance + is AccountStatus.Payment -> accountStatus.value.totalFiatBalance } } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt index 55e21d936c..6b0553047f 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt @@ -14,8 +14,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope -import timber.log.Timber private typealias SortingErrorByAccountId = MutableMap @@ -68,7 +68,7 @@ class ApplyTokenListSortingUseCase( maybeSortedAccountList.toEitherNeg() .onLeft { errorByAccountId -> - Timber.e( + TangemLogger.e( """ Unable to sort tokens for accounts: ${ errorByAccountId.entries.joinToString { "${it.key.value}: ${it.value}" } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt index 71bf409b07..4043b8821f 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt @@ -20,7 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -130,7 +130,7 @@ class GetAccountCurrencyByAddressUseCase( } return value ?: run { - Timber.d(message()) + TangemLogger.d(message()) raise(None) } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 5bb104ed00..e9656e1601 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -8,7 +8,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset @@ -22,10 +21,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching -import kotlinx.coroutines.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Use case for saving crypto currencies to a specific account. @@ -77,7 +80,7 @@ class ManageCryptoCurrenciesUseCase( skipDerivationErrors: Boolean = true, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { - Timber.d("No currencies to add or remove, skipping") + TangemLogger.d("No currencies to add or remove, skipping") return@eitherOn } @@ -89,7 +92,7 @@ class ManageCryptoCurrenciesUseCase( .modify(add = add, remove = remove) if (!modifiedCurrencyList.hasChanges) { - Timber.d("No changes in currencies, skipping") + TangemLogger.d("No changes in currencies, skipping") return@withContext } @@ -279,7 +282,7 @@ class ManageCryptoCurrenciesUseCase( createWalletManagers(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } - .onFailure { Timber.e(it, "Failed to sync tokens for wallet $userWalletId") } + .onFailure { TangemLogger.e("Failed to sync tokens for wallet $userWalletId", it) } } /** @@ -296,7 +299,7 @@ class ManageCryptoCurrenciesUseCase( runSuspendCatching { walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) } - .onFailure { Timber.e(it, "Failed to create wallet manager for network ${network.id}") } + .onFailure { TangemLogger.e("Failed to create wallet manager for network ${network.id}", it) } } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt index bb277d0707..b0d767f38a 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt @@ -5,11 +5,11 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.BalanceFetchingOperations import com.tangem.domain.tokens.FetchErrorFormatter import com.tangem.domain.tokens.FetchingSource +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap /** @@ -87,7 +87,7 @@ class CryptoCurrencyBalanceFetcher( ) if (errors.isNotEmpty()) { - Timber.e(FetchErrorFormatter.format(userWalletId, errors)) + TangemLogger.e(FetchErrorFormatter.format(userWalletId, errors)) } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt index 63ff52a802..6acd7e70de 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt @@ -11,7 +11,7 @@ import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder. import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Extension functions for retrieving [CryptoCurrency] from an [AccountList] or [Account.CryptoPortfolio]. @@ -57,7 +57,7 @@ object CryptoCurrencyOperations { val currencyId = catch( block = { CryptoCurrency.ID.fromValue(currencyIdValue) }, catch = { throwable -> - Timber.e("Error on converting currencyId: $throwable") + TangemLogger.e("Error on converting currencyId: $throwable") raise(None) }, ) diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index a7eff175d4..44e17e2ce6 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -23,8 +23,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.visa.models) + implementation(projects.core.utils) - implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt index 37ee742b77..f21ad15983 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger object EdSingleCurrencyCardConfig : CardConfig { @@ -14,7 +14,7 @@ object EdSingleCurrencyCardConfig : CardConfig { EllipticCurve.Ed25519 } else -> { - Timber.e("Unsupported blockchain, curve not found") + TangemLogger.e("Unsupported blockchain, curve not found") null } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt index 099c93ed10..eb9870b4f6 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class GenericCardConfig(maxWalletCount: Int) : CardConfig { @@ -25,7 +25,7 @@ class GenericCardConfig(maxWalletCount: Int) : CardConfig { EllipticCurve.Ed25519 } else -> { - Timber.e("Unsupported blockchain, curve not found") + TangemLogger.e("Unsupported blockchain, curve not found") null } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt index 8e661fffee..5ce2718892 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger object MultiWalletCardConfig : CardConfig { override val mandatoryCurves: List @@ -27,7 +27,7 @@ object MultiWalletCardConfig : CardConfig { EllipticCurve.Bls12381G2Aug } else -> { - Timber.e("Unsupported blockchain, curve not found") + TangemLogger.e("Unsupported blockchain, curve not found") null } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt index 0c4ba1c0b0..a545214f6f 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger data object Wallet2CardConfig : CardConfig { override val mandatoryCurves: List @@ -37,14 +37,14 @@ data object Wallet2CardConfig : CardConfig { // EllipticCurve.Ed25519 // } // else -> { - // Timber.e("Unsupported blockchain, curve not found") + // TangemLogger.e("Unsupported blockchain, curve not found") // null // } // } val curve = getPrimaryCurveForBlockchain(blockchain) // check curve supports if (!blockchain.getSupportedCurves().contains(curve)) { - Timber.e("Unsupported curve $curve for blockchain $blockchain") + TangemLogger.e("Unsupported curve $curve for blockchain $blockchain") return null } return curve diff --git a/domain/core/CLAUDE.md b/domain/core/CLAUDE.md new file mode 100644 index 0000000000..facad07dc2 --- /dev/null +++ b/domain/core/CLAUDE.md @@ -0,0 +1,77 @@ +# domain/core + +Cross-cutting domain utilities for async data loading, error handling, and reactive streams. Not business logic — foundational abstractions used across all domain modules. + +## LCE (Loading-Content-Error) Pattern + +`Lce` — sealed class representing async operation state: +- `Loading(partialContent?)` — in progress, may carry partial data +- `Content(content)` — success +- `Error(error)` — failure with typed error + +Key APIs: +- `lce { }` builder — executes block in `LceRaise` context with Arrow's Raise DSL for typed error handling +- `lceFlow { }` builder — creates `LceFlow` (alias for `Flow>`) via channel-based producer DSL +- `LceRaise.bind()` — extracts content from Lce/Either or short-circuits on error +- Extensions: `fold()`, `map()`, `mapError()`, `toLce()`, `toEither()` + +## Flow Packaging + +A pattern for complex data streams where work on a single flow is split into three logically separate components: **Producer** (creation), **Supplier** (delivery/caching), and **Fetcher** (refresh). Use it only when you need flexibility in creating, reusing, fetching, and updating a data stream (e.g., network status). Do NOT use for simple cases like reading preferences. + +### FlowProducer + +`FlowProducer` — creates the data flow. Implement: +- `fallback: Data` — emitted when the flow throws an exception +- `produce(): Flow` — the actual flow creation logic + +Built-in `produceWithFallback()` catches errors, emits `fallback`, waits 2s, then retries — keeping the flow alive for subscribers. + +`FlowProducer.Factory` — creates a Producer from params. Typically implemented via Hilt `@AssistedFactory`. + +**Implementation pattern:** +1. Define interface extending `FlowProducer` with inner `Params` data class and `Factory` interface +2. Create `Default*Producer` with `@AssistedInject` constructor taking `@Assisted params` + dependencies +3. Override `fallback` and `produce()` +4. Declare inner `@AssistedFactory` interface extending the Producer's Factory + +### FlowSupplier / FlowCachingSupplier + +`FlowSupplier` — delivers a flow by params via `operator fun invoke(params): Flow`. Also provides `getSyncOrNull(params, timeout)` for one-shot access. + +`FlowCachingSupplier` — abstract implementation that caches flows by key. Implement: +- `factory: FlowProducer.Factory` — to create producers +- `keyCreator: (Params) -> String` — to generate cache keys + +Behavior: returns cached flow if exists, otherwise creates via `factory.create(params).produceWithFallback()`, caches it, and auto-evicts on terminal exception. + +**Implementation pattern:** +1. Define abstract class extending `FlowCachingSupplier` with `factory` and `keyCreator` in constructor +2. In DI module, create anonymous subclass providing the factory (injected) and keyCreator lambda + +### FlowFetcher + +`FlowFetcher` — triggers data refresh, returns `Either`. Typically updates a store/data source, causing the Producer's flow to re-emit. + +**Implementation pattern:** +1. Define interface extending `FlowFetcher` with inner `Params` data class +2. Create `Default*Fetcher` with `@Inject` constructor, override `invoke` wrapping logic in `Either.catch { }`, handle errors with `.onLeft { }` + +### Testing + +- **FlowProducer**: test flow creation logic, params usage, emission behavior, exception handling +- **FlowFetcher**: test successful update path and error path (exception thrown) + +## Chain Processing + +- `Chain` / `ResultChain` — single operation in a chain, works with `Either` +- `ChainProcessor` — folds chains sequentially, stops on first error + +## Error Types + +- `DataError` — sealed domain error hierarchy: `NetworkError.NoInternetConnection`, `UserWalletError.WrongUserWallet` + +## Either Extensions + +- `Either.catchOn(dispatcher, block)` — executes on dispatcher, catches exceptions +- `eitherOn(dispatcher, block)` — Raise DSL block on specified dispatcher \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 1326610ec7..6d09dae413 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.reKotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) /** Testing libraries */ diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt index 9d5084361d..5299850377 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -2,8 +2,6 @@ package com.tangem.domain.redux interface StateDialog { - data object NfcFeatureIsUnavailable : StateDialog - data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog enum class ScanFailsSource { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index a0442452ef..79ab7d3f5a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -178,12 +178,15 @@ sealed interface Account { @Serializable data class Payment( override val accountId: AccountId, - override val accountName: AccountName, - val cryptoCurrencies: List, ) : Account { + override val accountName: AccountName.Custom = AccountName.Custom("Payment").getOrElse { + error("Can not create account name for Payment account with userWalletId = ${accountId.userWalletId}") + } - init { - error("Not yet implemented") + companion object { + operator fun invoke(userWalletId: UserWalletId): Payment { + return Payment(accountId = AccountId.forPaymentAccount(userWalletId = userWalletId)) + } } } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt index 31df2e7281..748f8de199 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -1,7 +1,6 @@ package com.tangem.domain.models.account import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList @@ -43,7 +42,7 @@ sealed interface AccountStatus { @Serializable data class Payment( override val account: Account.Payment, - val totalFiatBalance: TotalFiatBalance, + val value: PaymentAccountStatusValue, ) : AccountStatus } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt new file mode 100644 index 0000000000..fc44ccc105 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -0,0 +1,193 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +/** + * Represents the various states a payment account can have, encapsulating different information based on the state. + * + * @property source The source of the status information. + */ +@Serializable +sealed class PaymentAccountStatusValue { + abstract val source: StatusSource + + /** The total fiat balance associated with this status. */ + val totalFiatBalance: TotalFiatBalance + get() = when (this) { + is Error, + is IssuingCard, + is NotCreated, + is UnderReview, + -> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source) + is Loading -> TotalFiatBalance.Loading + is Locked -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + } + + /** + * Copies the status with a new [source]. + * + * @param source The new source of the status information. + */ + fun copySealed(source: StatusSource): PaymentAccountStatusValue { + return when (this) { + is IssuingCard -> copy(source = source) + is Loaded -> copy(source = source) + is Locked -> copy(source = source) + is UnderReview -> copy(source = source) + is Loading, + is NotCreated, + is Error, + -> this + } + } + + /** Represents the Loading state of a payment account, typically while fetching its details. */ + @Serializable + data object Loading : PaymentAccountStatusValue() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Represents a state where the payment account has not been created yet. */ + @Serializable + data object NotCreated : PaymentAccountStatusValue() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Represents a state where the payment account is under review (KYC). + * + * @property source The source of the status information. + * @property kycStatus The current KYC status. + * @property customerId The unique identifier of the customer. + */ + @Serializable + data class UnderReview( + override val source: StatusSource, + val kycStatus: KycStatus, + val customerId: String, + ) : PaymentAccountStatusValue() + + /** + * Represents a state where the card for the payment account is being issued. + * + * @property source The source of the status information. + */ + @Serializable + data class IssuingCard(override val source: StatusSource) : PaymentAccountStatusValue() + + /** + * Represents a state where the payment account is locked. + * + * @property source The source of the status information. + * @property customerId The unique identifier of the customer. + * @property cardId The unique identifier of the card. + * @property lastFourDigits The last four digits of the card number. + * @property currencyCode The code of the currency. + * @property depositAddress The address for deposits, if available. + * @property isPinSet Indicates if the PIN is set for the card. + * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + */ + @Serializable + data class Locked( + override val source: StatusSource, + val customerId: String, + val cardId: String, + val lastFourDigits: String, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + val fiatBalance: FiatBalance, + val cryptoBalance: CryptoBalance, + ) : PaymentAccountStatusValue() + + /** + * Represents a state where the payment account is successfully loaded with complete information. + * + * @property source The source of the status information. + * @property customerId The unique identifier of the customer. + * @property cardId The unique identifier of the card. + * @property lastFourDigits The last four digits of the card number. + * @property currencyCode The code of the currency. + * @property depositAddress The address for deposits, if available. + * @property isPinSet Indicates if the PIN is set for the card. + * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + */ + @Serializable + data class Loaded( + override val source: StatusSource, + val customerId: String, + val cardId: String, + val lastFourDigits: String, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + val fiatBalance: FiatBalance, + val cryptoBalance: CryptoBalance, + ) : PaymentAccountStatusValue() + + /** Represents an error state for the payment account status. */ + @Serializable + sealed class Error : PaymentAccountStatusValue() { + /** Error state indicating the device is exposed. */ + @Serializable + data object ExposedDevice : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Error state indicating the account is unavailable. */ + @Serializable + data object Unavailable : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Error state indicating the account data is not synced. */ + @Serializable + data object NotSynced : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Error state indicating that card issuance failed. + * + * @property customerId The unique identifier of the customer. + */ + @Serializable + data class CardIssueFailed(val customerId: String) : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + } + + /** + * Represents the fiat balance of the payment account. + * + * @property availableBalance The amount of available balance in fiat. + * @property currency The currency of the balance. + */ + @Serializable + data class FiatBalance(val availableBalance: SerializedBigDecimal, val currency: String) + + /** + * Represents the crypto balance of the payment account. + * + * @property id The unique identifier of the crypto asset. + * @property chainId The identifier of the blockchain network. + * @property depositAddress The address for deposits. + * @property tokenContractAddress The contract address of the token. + * @property balance The amount of the crypto balance. + */ + @Serializable + data class CryptoBalance( + val id: String, + val chainId: Long, + val depositAddress: String, + val tokenContractAddress: String, + val balance: SerializedBigDecimal, + ) +} \ No newline at end of file diff --git a/domain/search/.gitignore b/domain/search/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/search/build.gradle.kts b/domain/search/build.gradle.kts new file mode 100644 index 0000000000..e13e688d24 --- /dev/null +++ b/domain/search/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.search" +} + +dependencies { + api(projects.domain.core) + api(projects.domain.models) + implementation(projects.domain.common) + implementation(projects.domain.markets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency) + implementation(projects.domain.account) + implementation(projects.domain.account.status) +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt b/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt new file mode 100644 index 0000000000..d859f1d2a4 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * @property timestamp epoch milliseconds + */ +data class RecentSearchToken( + val id: CryptoCurrency.RawID, + val name: String, + val symbol: String, + val imageUrl: String?, + val timestamp: Long, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt new file mode 100644 index 0000000000..784f334bdb --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.markets.TokenMarket + +data class SearchResult( + val textHints: List, + val recentTokens: List, + val userAssets: List, + val marketTokens: List, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt new file mode 100644 index 0000000000..b949c8b3fa --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.search.model + +/** + * @property timestamp epoch milliseconds + */ +data class SearchTextHint(val text: String, val timestamp: Long) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt new file mode 100644 index 0000000000..fd64a23e24 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId + +data class UserAssetSearchEntry( + val userWalletId: UserWalletId, + val userWalletName: String, + val accountId: AccountId, + val accountName: AccountName, + val currencyStatus: CryptoCurrencyStatus, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt b/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt new file mode 100644 index 0000000000..f7cea5e075 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.search.repository + +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import kotlinx.coroutines.flow.Flow + +/** + * Repository responsible for managing local search history storage. + * Handles persistence of user's past search queries and recently viewed market tokens. + * Each history type is limited to 3 entries, sorted by timestamp in descending order. + */ +interface SearchRepository { + + /** Observes the list of saved text hints, sorted by timestamp descending. */ + fun getTextHints(): Flow> + + /** Observes the list of recently viewed market tokens, sorted by timestamp descending. */ + fun getRecentTokens(): Flow> + + /** + * Saves a text hint to the search history. + * If the hint already exists, its timestamp is updated. Oldest entries are evicted when the limit is exceeded. + * + * @param text the search query text to save + */ + suspend fun saveTextHint(text: String) + + /** + * Saves a recently viewed market token to the search history. + * If a token with the same ID already exists, it is moved to the top. Oldest entries are evicted when the limit + * is exceeded. + * + * @param token the market token entry to save + */ + suspend fun saveRecentToken(token: RecentSearchToken) + + /** Clears all search history, including both text hints and recent tokens. */ + suspend fun clearHistory() +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt new file mode 100644 index 0000000000..f12c208257 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.repository.SearchRepository + +/** + * Clears the entire search history, removing both text hints and recently viewed tokens. + * + * @property searchRepository local search history storage + */ +class ClearSearchHistoryUseCase( + private val searchRepository: SearchRepository, +) { + + suspend operator fun invoke() { + searchRepository.clearHistory() + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt new file mode 100644 index 0000000000..87e48a0215 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -0,0 +1,126 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.search.model.SearchResult +import com.tangem.domain.search.model.UserAssetSearchEntry +import com.tangem.domain.search.repository.SearchRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf + +/** + * Primary search use case that produces [SearchResult] based on the current query. + * + * Behavior depends on the query: + * - **Empty query** — returns search history: text hints and recently viewed tokens. + * - **Non-empty query** — performs the search across all unlocked user wallets, + * matching currencies by name or symbol, and combines the results with externally provided market tokens. + * + * @property searchRepository local search history storage + * @property multiAccountStatusListSupplier supplier for loaded account status lists across all wallets + * @property userWalletsListRepository repository providing the list of user wallets + */ +class GetSearchResultsUseCase( + private val searchRepository: SearchRepository, + private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + private val userWalletsListRepository: UserWalletsListRepository, +) { + + /** + * Produces a [Flow] of [SearchResult] for the given [query]. + * + * @param query the search query string; blank means "show history" + * @param marketTokens external flow of market token search results (provided by presentation layer) + */ + operator fun invoke( + query: String, + marketTokens: Flow> = flowOf(emptyList()), + ): Flow { + return if (query.isBlank()) { + observeHistory() + } else { + searchAssets(query, marketTokens) + } + } + + private fun observeHistory(): Flow { + return combine( + searchRepository.getTextHints(), + searchRepository.getRecentTokens(), + ) { hints, tokens -> + SearchResult( + textHints = hints, + recentTokens = tokens, + userAssets = emptyList(), + marketTokens = emptyList(), + ) + } + } + + private fun searchAssets(query: String, marketTokens: Flow>): Flow { + return combine( + observeUserAssets(query), + marketTokens, + ) { userAssets, markets -> + SearchResult( + textHints = emptyList(), + recentTokens = emptyList(), + userAssets = userAssets, + marketTokens = markets, + ) + } + } + + private fun observeUserAssets(query: String): Flow> { + val lowerQuery = query.lowercase() + return combine( + multiAccountStatusListSupplier(), + userWalletsListRepository.userWallets, + ) { statusLists, wallets -> + val unlockedWallets = wallets + .orEmpty() + .filterNot(UserWallet::isLocked) + .associateBy { it.walletId } + + if (unlockedWallets.isEmpty()) return@combine emptyList() + + statusLists + .filter { it.userWalletId in unlockedWallets } + .flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) } + } + } + + private fun extractMatchingAssets( + statusList: AccountStatusList, + wallets: Map, + lowerQuery: String, + ): List { + val wallet = wallets[statusList.userWalletId] ?: return emptyList() + return statusList.accountStatuses + .filterCryptoPortfolio() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + .filter { currencyStatus -> + val name = currencyStatus.currency.name.lowercase() + val symbol = currencyStatus.currency.symbol.lowercase() + name.contains(lowerQuery) || symbol.contains(lowerQuery) + } + .map { currencyStatus -> + UserAssetSearchEntry( + userWalletId = statusList.userWalletId, + userWalletName = wallet.name, + accountId = accountStatus.accountId, + accountName = accountStatus.account.accountName, + currencyStatus = currencyStatus, + ) + } + } + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt new file mode 100644 index 0000000000..9c8074fd7c --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.repository.SearchRepository + +/** + * Saves a market token to the "recently viewed" search history. + * Only market assets should be saved (user's own assets are ignored). + * The history is limited to 3 entries; oldest entries are evicted automatically. + * + * @property searchRepository local search history storage + */ +class SaveRecentSearchTokenUseCase( + private val searchRepository: SearchRepository, +) { + + /** + * @param token the market token to persist as a recent search entry + */ + suspend operator fun invoke(token: RecentSearchToken) { + searchRepository.saveRecentToken(token) + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt new file mode 100644 index 0000000000..486d9f40bd --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.repository.SearchRepository + +/** + * Saves the current search query text to the local search history. + * Blank queries are ignored. The history is limited to 3 entries; oldest entries are evicted automatically. + * Should be called when the user selects any asset from the search results. + * + * @property searchRepository local search history storage + */ +class SaveSearchQueryUseCase( + private val searchRepository: SearchRepository, +) { + + /** + * @param query the search text to persist; blank values are silently ignored + */ + suspend operator fun invoke(query: String) { + if (query.isBlank()) return + searchRepository.saveTextHint(query.trim()) + } +} \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 7cf130837a..587e19d32d 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.jodatime) - implementation(deps.timber) implementation(projects.domain.legacy) implementation(projects.domain.walletManager) // TODO refactor to use from data module diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt index dc87e3b8ed..3a41d1eee8 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.indexOfFirstOrNull -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Producer of staking balance for selected wallet [UserWalletId] @@ -51,9 +51,8 @@ interface SingleStakingBalanceProducer : FlowProducer { ), ) - Timber.e( - "Multiple balances found for staking ID $currentStakingId:\n%s", - currentBalances.joinToString("\n"), + TangemLogger.e( + "Multiple balances found for staking ID $currentStakingId:\n${currentBalances.joinToString("\n")}", ) val dataIndex = currentBalances.indexOfFirstOrNull { it is StakingBalance.Data } @@ -66,7 +65,7 @@ interface SingleStakingBalanceProducer : FlowProducer { } else { val balance = currentBalances.firstOrNull() ?: return null - Timber.i("Staking balance found for $currentStakingId:\n$balance") + TangemLogger.i("Staking balance found for $currentStakingId:\n$balance") balance } } diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt index 3a65c92b9c..bd321cbf88 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -8,7 +8,7 @@ import java.math.BigDecimal * * @property provider swap provider * @property toTokenAmount amount of token you want to receive - * @property fromTokenAmount amount of from-token required (for fixed rate quotes) + * @property fromTokenAmount amount of from-token required (only set for fixed rate quotes) * @property allowanceContract whether swap occurs via third token */ data class SwapQuoteModel( @@ -16,4 +16,5 @@ data class SwapQuoteModel( val toTokenAmount: BigDecimal, val fromTokenAmount: BigDecimal?, val allowanceContract: String?, + val quoteId: String? = null, ) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index b456087ac4..dacec2c058 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -91,6 +91,7 @@ interface SwapRepositoryV2 { expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String?, ): SwapDataModel /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt index 448e5ddcd5..2726e77e94 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -30,6 +30,7 @@ class GetSwapDataUseCase( expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String? = null, ): Either = Either.catch { swapRepositoryV2.getSwapData( userWallet = userWallet, @@ -42,6 +43,7 @@ class GetSwapDataUseCase( expressProvider = expressProvider, rateType = rateType, expressOperationType = expressOperationType, + quoteId = quoteId, ) }.mapLeft { throwable -> swapErrorResolver.resolve(throwable) diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 52ba7c414e..236ce7785f 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -53,7 +53,6 @@ dependencies { /** Android - Other */ implementation(deps.androidx.paging.runtime) - implementation(deps.timber) /** Utils */ implementation(deps.jodatime) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt index 96df850a31..9538efb817 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt @@ -8,10 +8,10 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import timber.log.Timber /** * Shared utility for fetching cryptocurrency balance data from multiple sources. @@ -116,14 +116,14 @@ class BalanceFetchingOperations( val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = currency) if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { - Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}") + TangemLogger.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}") } stakingId.getOrNull() } if (stakingIds.isEmpty()) { - Timber.i("No staking IDs found for user wallet $userWalletId") + TangemLogger.i("No staking IDs found for user wallet $userWalletId") return Unit.right() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index afb0384174..27a9af10ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -26,10 +26,10 @@ import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import timber.log.Timber /** * Fetcher of wallet balance by [UserWalletId] @@ -184,7 +184,7 @@ class WalletBalanceFetcher internal constructor( check(errors.isEmpty()) { val message = FetchErrorFormatter.formatWalletErrors(userWalletId, errors) - Timber.e(message) + TangemLogger.e(message) message } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt index e7f95702ce..4bbf8f3557 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt @@ -2,14 +2,14 @@ package com.tangem.domain.tokens.wallet.implementor import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.MultiWalletAccountListFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.wallet.BaseWalletBalanceFetcher import com.tangem.domain.tokens.wallet.WalletFetchingSource +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.firstOrNull -import timber.log.Timber /** * Implementation of [BaseWalletBalanceFetcher] for MULTI-CURRENCY wallet @@ -37,7 +37,7 @@ internal class MultiWalletBalanceFetcher( multiWalletAccountListFetcher( params = MultiWalletAccountListFetcher.Params(userWalletId = userWalletId), ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } return multiWalletCryptoCurrenciesSupplier( params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), diff --git a/domain/tokensync/build.gradle.kts b/domain/tokensync/build.gradle.kts index bdf6721531..35464995f3 100644 --- a/domain/tokensync/build.gradle.kts +++ b/domain/tokensync/build.gradle.kts @@ -16,5 +16,4 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) - implementation(deps.timber) } \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt index d3f8fb0c31..ddf41c5794 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt @@ -6,9 +6,9 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokensync.repository.TokenSyncRepository import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap class SyncTokensUseCase( @@ -26,7 +26,7 @@ class SyncTokensUseCase( tokenSyncRepository.runSync(userWalletId) applyDiscoveredTokens(userWalletId) } catch (e: Exception) { - Timber.e(e, "Token sync failed for wallet: $userWalletId") + TangemLogger.e("Token sync failed for wallet: $userWalletId", e) } finally { activeSyncJobs.remove(userWalletId) } @@ -50,7 +50,7 @@ class SyncTokensUseCase( } } } catch (e: Exception) { - Timber.e(e, "Failed to apply pending syncs") + TangemLogger.e("Failed to apply pending syncs", e) } } } @@ -70,7 +70,7 @@ class SyncTokensUseCase( true }, ifLeft = { error -> - Timber.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error") + TangemLogger.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error") false }, ) diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 3c13ebe644..ec2d1affc7 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { implementation(deps.spongecastle.core) /** Libs - Other */ - implementation(deps.timber) implementation(deps.jodatime) implementation(deps.androidx.paging.runtime) implementation(deps.moshi) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt deleted file mode 100644 index bdc604087f..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.pay - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.kyc.KycStatus -import com.tangem.domain.models.serialization.SerializedBigDecimal -import kotlinx.serialization.Serializable - -@Serializable -sealed class PaymentAccountStatus { - - abstract val source: StatusSource - - @Serializable - data object Loading : PaymentAccountStatus() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data object NotCreated : PaymentAccountStatus() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data class UnderReview( - override val source: StatusSource, - val kycStatus: KycStatus, - ) : PaymentAccountStatus() - - @Serializable - data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus() - - @Serializable - data class Locked(override val source: StatusSource) : PaymentAccountStatus() - - @Serializable - data class Loaded( - override val source: StatusSource, - val cardId: String, - val lastFourDigits: String, - val balance: SerializedBigDecimal, - val currencyCode: String, - val depositAddress: String?, - val isPinSet: Boolean, - ) : PaymentAccountStatus() - - @Serializable - sealed class Error : PaymentAccountStatus() { - @Serializable - data object ExposedDevice : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data class Unavailable(override val source: StatusSource) : Error() - - @Serializable - data object NotSynced : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data object CardIssueFailed : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - } -} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt index 5f3223e514..1004e00447 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt @@ -8,4 +8,5 @@ import com.tangem.domain.models.wallet.UserWallet interface TangemPayCryptoCurrencyFactory { fun create(userWallet: UserWallet, chainId: Int): Either + fun create(userWallet: UserWallet): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt index c49a25f45d..313e5741ce 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt @@ -1,10 +1,10 @@ package com.tangem.domain.pay.flow import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus -interface PaymentAccountStatusProducer : FlowProducer { +interface PaymentAccountStatusProducer : FlowProducer { data class Params(val userWalletId: UserWalletId) interface Factory : FlowProducer.Factory diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt index 94580d26e1..11744a357c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt @@ -1,10 +1,18 @@ package com.tangem.domain.pay.flow import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow @Suppress("UnnecessaryAbstractClass") abstract class PaymentAccountStatusSupplier( override val factory: PaymentAccountStatusProducer.Factory, override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + operator fun invoke(userWalletId: UserWalletId): Flow { + val params = PaymentAccountStatusProducer.Params(userWalletId) + return this.invoke(params) + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 2fd02e7a45..11ef7e59c7 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -1,6 +1,8 @@ package com.tangem.domain.pay.model +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.visa.model.TangemPayCardFrozenState import java.math.BigDecimal sealed class MainCustomerInfoContentState { @@ -25,6 +27,7 @@ data class CustomerInfo( data class ProductInstance( val id: String, val cardId: String, + val frozenState: TangemPayCardFrozenState, ) data class CardInfo( @@ -33,5 +36,7 @@ data class CustomerInfo( val currencyCode: String, val depositAddress: String?, val isPinSet: Boolean, + val fiatBalance: PaymentAccountStatusValue.FiatBalance, + val cryptoBalance: PaymentAccountStatusValue.CryptoBalance, ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index da2687589a..180b1b864f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -12,10 +12,8 @@ import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* -import timber.log.Timber - -private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" class TangemPayMainScreenCustomerInfoUseCase( private val onboardingRepository: OnboardingRepository, @@ -27,13 +25,15 @@ class TangemPayMainScreenCustomerInfoUseCase( val state: StateFlow>> field = MutableStateFlow(value = mapOf()) + private val logger = TangemLogger.withTag("TangemPayMainScreenCustomerInfoUseCase") + suspend fun fetch(userWalletId: UserWalletId) { - Timber.tag(TAG).i("fetch: ${userWalletId.stringValue}") + logger.i("fetch: ${userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") + logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") + logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) return // fast exit @@ -42,7 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase( onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> - Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") + logger.e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") if (error is VisaApiError.NotPaeraCustomer) { showOnboardingBannerIfEligible(userWalletId) } else { @@ -50,7 +50,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } }, ifRight = { hasTangemPay -> - Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") + logger.i("checkCustomerWallet for $userWalletId: $hasTangemPay") if (hasTangemPay) { val oldResult = state.value[userWalletId] if (oldResult == null) { @@ -125,14 +125,13 @@ class TangemPayMainScreenCustomerInfoUseCase( ): Either { return onboardingRepository.getCustomerInfo(userWalletId) .mapLeft { error -> - Timber.tag(TAG).e("mapErrorForCustomer: $error") + logger.e("mapErrorForCustomer: $error") error.mapErrorForCustomer() } .map { customerInfo -> - Timber.tag(TAG).i("customerInfo") + logger.i("customerInfo") if (customerInfo.productInstance == null) { onboardingRepository.createOrder(userWalletId) - Timber.tag("ddk9499").d("TangemPayMainScreenCustomerInfoUseCase.proceedWithoutOrder: ") MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW) } else { MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.COMPLETED) diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 47d5fd6e1c..4d61929513 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { /** Other libraries */ implementation(platform(deps.firebase.bom)) implementation(deps.firebase.analytics) - implementation(deps.timber) // region DI implementation(deps.hilt.android) diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 1af28af669..83c2d1f222 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -12,13 +12,14 @@ dependencies { // endregion // region Domain modules - implementation(project(":domain:models")) + implementation(projects.domain.models) // endregion + implementation(projects.core.utils) + // region Other libraries implementation(deps.kotlin.serialization) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) // endregion } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt deleted file mode 100644 index cccba8a293..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import com.tangem.common.CompletionResult -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow - -interface UserWalletsListManager { - - /** - * Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable] - * */ - val isLockable: Boolean - - /** [Flow] with all saved [UserWallet]s updates */ - val userWallets: Flow> - - /** Count saved wallets updates */ - val savedWalletsCount: Flow - - /** [Flow] with selected [UserWallet] updates */ - @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") - val selectedUserWallet: Flow - - /** [List] with all saved [UserWallet]s updates */ - val userWalletsSync: List - - /** Selected [UserWallet] */ - @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") - val selectedUserWalletSync: UserWallet? - - /** Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet] */ - val hasUserWallets: Boolean - - /** Count of saved user wallets */ - val walletsCount: Int - - /** - * Set [UserWallet] with provided [UserWalletId] as selected - * - * @param userWalletId [UserWalletId] of [UserWallet] which must be selected - * - * @return [CompletionResult.Success] with selected [UserWallet] or [CompletionResult.Failure] with - * [NoSuchElementException] if [UserWallet] with [userWalletId] not found - */ - suspend fun select(userWalletId: UserWalletId): CompletionResult - - /** - * Save provided user wallet and set it as selected - * - * @param userWallet [UserWallet] to save - * @param canOverride If false, then terminate with [UserWalletsListError.WalletAlreadySaved] when user tries - * to save an already saved card - * - * @return [CompletionResult] of operation - */ - suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult - - /** - * Same as [save] but not change selected user wallet ID and not terminate with - * [UserWalletsListError.WalletAlreadySaved] if [UserWallet] already saved. - * Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]. - * - * @param userWalletId update [UserWallet] with that [UserWalletId] - * @param update lambda that receives stored [UserWallet] and returns updated [UserWallet] - * - * @return [CompletionResult.Success] with updated [UserWallet] or [CompletionResult.Failure] with - * [NoSuchElementException] if [UserWallet] with [userWalletId] not found - */ - suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult - - /** - * Delete saved [UserWallet]s with provided [UserWalletId]s. - * Sets [isLocked] as true if [userWallets] is empty or if all [userWallets] are locked. - * - * @param userWalletIds [UserWalletId]s of [UserWallet]s which must be deleted - * - * @return [CompletionResult] of operation - */ - suspend fun delete(userWalletIds: List): CompletionResult - - /** - * Clear all saved [UserWallet]s and set [isLocked] as true - * - * @return [CompletionResult] of operation - */ - suspend fun clear(): CompletionResult - - /** - * Get [UserWallet] with provided [UserWalletId] - * - * @return [CompletionResult.Success] with found [UserWallet] or [CompletionResult.Failure] with - * [NoSuchElementException] if [UserWallet] with [userWalletId] not found - */ - suspend fun get(userWalletId: UserWalletId): CompletionResult - - interface Lockable : UserWalletsListManager { - - /** - * Indicates that all [UserWallet]s is locked - * - * @see [isLocked] - * @see [UserWallet.isLocked] - */ - val lockedState: Flow - - /** - * Indicates that all [UserWallet]s is locked. Sync version. - * - * @see [lockedState] - * @see [UserWallet.isLocked] - */ - val isLocked: Boolean - - /** - * Receive saved [UserWallet]s, populate [userWallets] flow with it and set [lockedState] as false. - * - * @param type Defines the behavior of the operation. - * - * @return [CompletionResult] of operation, with selected [UserWallet] - * or null if there is no selected [UserWallet] - */ - suspend fun unlock(type: UnlockType): CompletionResult - - /** Remove [UserWallet]s from [userWallets] and set [lockedState] as true */ - fun lock() - - /** - * Defines the behavior of the [unlock] operation. - * */ - enum class UnlockType { - /** - * Ensures that all stored [UserWallet]s are unlocked, - * or throws [UserWalletsListError.NotAllUserWalletsUnlocked]. - * - * In this type [selectedUserWallet] is either a previously selected [UserWallet] or the first stored - * [UserWallet]. - * */ - ALL, - - /** - * Ensures that at least one stored [UserWallet] is unlocked, - * or throws [UserWalletsListError.NoUserWalletSelected]. - * - * In this type [selectedUserWallet] is the first stored and unlocked [UserWallet]. - * */ - ANY, - - /** - * Same as [ALL] type, but this type can not change [selectedUserWallet] while unlocking. - * */ - ALL_WITHOUT_SELECT, - } - } - - // For provider - companion object -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt index 1f111425fc..063dd3380d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.runSuspendCatching -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Use case to sync wallet with remote @@ -16,6 +16,6 @@ class SyncWalletWithRemoteUseCase( suspend operator fun invoke(userWalletId: UserWalletId) { runSuspendCatching { walletsRepository.createWallet(userWalletId) } - .onFailure(Timber::e) + .onFailure { TangemLogger.e("Error", it) } } } \ No newline at end of file diff --git a/domain/yield-supply/build.gradle.kts b/domain/yield-supply/build.gradle.kts index e8a423d566..ea83aa3225 100644 --- a/domain/yield-supply/build.gradle.kts +++ b/domain/yield-supply/build.gradle.kts @@ -15,6 +15,7 @@ tasks.withType().configureEach { dependencies { /** Core */ implementation(projects.core.ui) + implementation(projects.core.utils) /** Domain */ implementation(projects.domain.account.status) @@ -34,7 +35,6 @@ dependencies { /** Other */ implementation(deps.arrow.core) - implementation(deps.timber) /** tests */ testImplementation(projects.common.test) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt index 9a9da874ed..71f832affc 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt @@ -14,7 +14,7 @@ import com.tangem.domain.yield.supply.INCREASE_GAS_LIMIT_FOR_SUPPLY import com.tangem.domain.yield.supply.fixFee import com.tangem.domain.yield.supply.increaseGasLimitBy import com.tangem.utils.extensions.isSingleItem -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class YieldSupplyEstimateEnterFeeUseCase( private val feeRepository: FeeRepository, @@ -65,7 +65,7 @@ class YieldSupplyEstimateEnterFeeUseCase( val estimatedFees = blockAidGasEstimate.getGasEstimation( cryptoCurrency = cryptoCurrency, transactionDataList = transactionDataList, - ).onLeft(Timber::e).getOrNull() ?: return null + ).onLeft { TangemLogger.e("Error", it) }.getOrNull() ?: return null if (estimatedFees.estimatedGasList.isEmpty()) return null diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt index 8693bae476..9b202cc6ed 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt @@ -4,14 +4,15 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.yield.supply.YieldSupplyRepository +import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker.Companion.CHECK_INTERVAL_MS import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap /** @@ -73,7 +74,7 @@ class YieldSupplyPendingTracker( try { checkAllTracked() } catch (ex: Exception) { - Timber.e(ex) + TangemLogger.e("Error", ex) } } } diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts index b6439a01c3..1c4ab8d7a1 100644 --- a/features/account/impl/build.gradle.kts +++ b/features/account/impl/build.gradle.kts @@ -66,7 +66,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index cde8aabbcb..57ca6f87e6 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -25,10 +25,10 @@ import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -147,7 +147,7 @@ internal class ArchivedAccountListModel @Inject constructor( private fun logError(error: AccountFeatureError, params: Map = emptyMap()) { val exception = IllegalStateException(error.toString()) - Timber.e(exception) + TangemLogger.e("Error", exception) analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent(exception = exception, params = params), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt index 737b5f6e86..2c64642433 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt @@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.usecase.ArchivedAccountList import com.tangem.domain.models.account.AccountId +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber import javax.inject.Inject internal class AccountArchivedUMBuilder @Inject constructor() { @@ -49,7 +49,7 @@ internal class AccountArchivedUMBuilder @Inject constructor() { onCloseClick: () -> Unit, getArchivedAccounts: () -> Unit, ): AccountArchivedUM.Error { - Timber.e(throwable) + TangemLogger.e("Error", throwable) return AccountArchivedUM.Error( onCloseClick = onCloseClick, onRetryClick = { getArchivedAccounts() }, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 5ef205ef80..50a4eaf8b6 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -42,11 +42,11 @@ import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder. import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -242,7 +242,7 @@ internal class AccountCreateEditModel @Inject constructor( private fun onNameChange(name: AccountNameUM) { val isNotEmptyCustomName = (name as? AccountNameUM.Custom)?.raw?.isNotEmpty() == true if (!name.isValidName() && isNotEmptyCustomName) { - Timber.d("Invalid account name: $name") + TangemLogger.d("Invalid account name: $name") return } @@ -310,7 +310,7 @@ internal class AccountCreateEditModel @Inject constructor( private fun logError(error: AccountFeatureError, params: Map = emptyMap()) { val exception = IllegalStateException(error.toString()) - Timber.e(exception) + TangemLogger.e("Error", exception) analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent(exception = exception, params = params), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index d7e351ff96..b70b65e101 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.PortfolioFetcher @@ -179,13 +180,10 @@ internal class PortfolioSelectorModel @Inject constructor( } } - portfolio.accountsBalance.accountStatuses.forEach { accountStatus -> + portfolio.accountsBalance.accountStatuses.filterCryptoPortfolio().forEach { accountStatus -> val isEnabledByFeature = isEnabled(wallet, accountStatus) val account = accountStatus.account - val accountBalance = when (accountStatus) { - is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.totalFiatBalance - } + val accountBalance = accountStatus.tokenList.totalFiatBalance val accountItemUM = AccountPortfolioItemUMConverter( onClick = { selectorController.selectAccount(account.accountId) }, appCurrency = appCurrency, diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts index e15b2662cd..2e43f87aee 100644 --- a/features/approval/impl/build.gradle.kts +++ b/features/approval/impl/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { /** Other */ implementation(deps.decompose) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.kotlin.immutable.collections) implementation(deps.arrow.core) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 87882d24ce..3d328cc6bf 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -8,6 +8,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -15,6 +16,7 @@ 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.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.models.currency.CryptoCurrency @@ -27,19 +29,17 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.common.ui.userwallet.ext.walletInterationIcon -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -183,7 +183,7 @@ internal class GiveApprovalModel @Inject constructor( contractAddress = tokenCurrency.contractAddress, spenderAddress = params.spenderAddress, ).getOrElse { error -> - Timber.e(error, "Failed to create approval transaction") + TangemLogger.e("Failed to create approval transaction", error) return false } @@ -201,7 +201,7 @@ internal class GiveApprovalModel @Inject constructor( ) }.fold( ifLeft = { error -> - Timber.e("Failed to send approval transaction: $error") + TangemLogger.e("Failed to send approval transaction: $error") false }, ifRight = { diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index def97bf1f3..075c304c23 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.configToggles) implementation(projects.core.navigation) + implementation(projects.core.utils) /** Domain */ implementation(projects.domain.wallets) @@ -45,7 +46,6 @@ dependencies { } /** Other */ - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 78cd72167e..116d0fd053 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -22,13 +22,13 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -88,7 +88,7 @@ internal class AskBiometryModel @Inject constructor( * because it will be automatically saved on UserWalletsListManager switch */ val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run { - Timber.e("Unable to save user wallet") + TangemLogger.e("Unable to save user wallet") uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), ) diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 458b4b5048..8fd9175d79 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -68,7 +68,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts index 1578c1916f..c280d4160c 100644 --- a/features/create-wallet-start/impl/build.gradle.kts +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -66,7 +66,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index ac00b3703e..5f590c887c 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -18,7 +18,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.ScanCardProcessor @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val HIDE_PROGRESS_DELAY = 400L @@ -211,7 +211,7 @@ internal class CreateWalletStartModel @Inject constructor( val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return } @@ -221,7 +221,7 @@ internal class CreateWalletStartModel @Inject constructor( delay(HIDE_PROGRESS_DELAY) setLoading(false) when (error) { - is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet") + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, @@ -246,17 +246,12 @@ internal class CreateWalletStartModel @Inject constructor( private fun handleScanError(error: TangemError) { when (error) { is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() - is TangemSdkError -> Timber.e(error, "Scan error occurred") - else -> Timber.e(error, "Error happened") + is TangemSdkError -> TangemLogger.e("Scan error occurred", error) + else -> TangemLogger.e("Error happened", error) } } private fun handleNfcFeatureUnavailable() { - uiMessageSender.send( - message = DialogMessage( - message = resourceReference(R.string.nfc_error_unavailable), - title = resourceReference(id = R.string.common_error), - ), - ) + uiMessageSender.send(Dialogs.nfcFeatureUnavailable()) } } \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 19d925cad1..742988403b 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.core.analytics.models) implementation(projects.common.routing) implementation(projects.common.ui) @@ -78,7 +79,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.reKotlin) - implementation(deps.timber) implementation(deps.arrow.core) implementation(deps.arrow.fx) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index fb9104d96e..a4d4c58b0a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.model import android.content.res.Resources import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -84,8 +84,8 @@ internal class DetailsModel @Inject constructor( val isWalletConnectAvailable = runBlocking { // danger region, this works immediately, but will be refactored later with WC - checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { - Timber.w("Unable to check WalletConnect availability: $it") + checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { throwable -> + TangemLogger.w("Unable to check WalletConnect availability: $throwable") false } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 44af704d49..5c00ab323d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -115,7 +115,7 @@ internal class UserWalletListModel @Inject constructor( unlockWalletUseCase(userWalletId) .onRight { router.push(AppRoute.WalletSettings(userWalletId)) } .onLeft { error -> - Timber.e("Failed to unlock wallet $userWalletId: $error") + TangemLogger.e("Failed to unlock wallet $userWalletId: $error") error.handle( onUserCancelled = {}, isFromUnlockAll = false, @@ -142,7 +142,7 @@ internal class UserWalletListModel @Inject constructor( applyUserWalletListSortingUseCase(userWalletIds).onRight { analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder()) }.onLeft { error -> - Timber.e("Failed to apply wallet list sorting: $error") + TangemLogger.e("Failed to apply wallet list sorting: $error") } } } diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index 7c72408c05..58d95cba7f 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { /** Other dependencies */ implementation(deps.arrow.core) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt index 5265f434bf..110e0ae876 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -5,7 +5,7 @@ import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebView import com.google.accompanist.web.AccompanistWebViewClient -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * [AccompanistWebViewClient] for Disclaimer [WebView] @@ -24,7 +24,7 @@ internal class DisclaimerWebViewClient( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - Timber.d("onPageStarted: $url") + TangemLogger.d("onPageStarted: $url") if (url != null) { loadedUrls[url] = false } @@ -33,7 +33,7 @@ internal class DisclaimerWebViewClient( override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) - Timber.d("onPageFinished: $url") + TangemLogger.d("onPageFinished: $url") if (url != null && loadedUrls.containsKey(url)) { loadedUrls[url] = true onLoadingFinished(false) @@ -45,7 +45,7 @@ internal class DisclaimerWebViewClient( val url = request?.url?.toString() - Timber.d("onReceivedError: $url") + TangemLogger.d("onReceivedError: $url") if (url != null && loadedUrls.containsKey(url)) { loadedUrls[url] = true diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index 95ac903999..2e076661b9 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -75,7 +75,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.decompose.ext.compose) /* Core */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index 9b8a539048..9538938a8c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -78,6 +78,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( screenSource = AnalyticsParam.ScreensSources.Token, ) }, + onMarketOpenClick = { onMarketOpenClick(null) }, ), ), ) @@ -136,7 +137,10 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( override fun onOpenEarnPage() { innerRouter.push( FeedEntryChildFactory.Child.Earn( - params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }), + params = DefaultEarnComponent.Params( + onBackClick = { onChildBack() }, + onMarketOpenClick = { onMarketOpenClick(null) }, + ), ), ) } @@ -240,6 +244,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( paginationConfig = null, ) }, + onMarketOpenClick = { clickIntents.onMarketOpenClick(null) }, ), ) FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index a0b7dcb1e8..e5915d7103 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -1,9 +1,17 @@ package com.tangem.features.feed.components.earn +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState @@ -17,11 +25,15 @@ import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.components.feed.FeedBottomSheetRoute import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.EarnModel +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent import kotlinx.serialization.Serializable @@ -44,15 +56,41 @@ internal class DefaultEarnComponent( override fun Title(bottomSheetState: State) { val background = LocalMainBottomSheetColor.current.value val state by earnModel.state.collectAsStateWithLifecycle() - TangemTopAppBar( - containerColor = background, - title = stringResourceSafe(R.string.earn_title), - startButton = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_back_24, - onClicked = state.onBackClick, - isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, - ), - ) + if (LocalRedesignEnabled.current) { + FeedSearchBar( + isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, + feedListSearchBar = state.feedListSearchBar, + modifier = Modifier.drawBehind { drawRect(background) }, + startContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = state.onBackClick, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2), + ) + }, + ) + } else { + TangemTopAppBar( + containerColor = background, + title = stringResourceSafe(R.string.earn_title), + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = state.onBackClick, + isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ), + ) + } } @Composable @@ -94,5 +132,6 @@ internal class DefaultEarnComponent( @Serializable data class Params( val onBackClick: () -> Unit, + val onMarketOpenClick: () -> Unit, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index d43ace2233..93292cc527 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -1,10 +1,18 @@ package com.tangem.features.feed.components.market.details +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network @@ -12,9 +20,13 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency @@ -22,6 +34,7 @@ import com.tangem.features.feed.components.market.details.portfolio.api.MarketsP import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent import com.tangem.features.feed.model.market.details.state.TokenNetworksState +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar import kotlinx.coroutines.flow.collectLatest @@ -86,15 +99,59 @@ internal class DefaultMarketsTokenDetailsComponent( override fun Title(bottomSheetState: State) { val state by model.state.collectAsStateWithLifecycle() val background = LocalMainBottomSheetColor.current.value - MarketsTokenDetailsTopBar( - onBackClick = { params.onBackClicked() }, - isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, - shouldShowPriceSubtitle = state.shouldShowPriceSubtitle, - tokenName = state.tokenName, - tokenPrice = state.priceText, - backgroundColor = background, - onShareClick = state.onShareClick, - ) + if (LocalRedesignEnabled.current) { + FeedSearchBar( + isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, + feedListSearchBar = state.feedListSearchBar, + modifier = Modifier.drawBehind { drawRect(background) }, + startContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = { params.onBackClicked() }, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2), + ) + }, + endContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_share_new_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = state.onShareClick, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2_5), + ) + }, + ) + } else { + MarketsTokenDetailsTopBar( + onBackClick = { params.onBackClicked() }, + isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, + shouldShowPriceSubtitle = state.shouldShowPriceSubtitle, + tokenName = state.tokenName, + tokenPrice = state.priceText, + backgroundColor = background, + onShareClick = state.onShareClick, + ) + } } @Composable @@ -131,6 +188,7 @@ internal class DefaultMarketsTokenDetailsComponent( val analyticsParams: AnalyticsParams?, val onBackClicked: () -> Unit, val onArticleClick: (articleId: Int, preselectedArticlesId: List) -> Unit, + val onMarketOpenClick: () -> Unit, ) @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index a6b02044fb..946ab12577 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -8,6 +8,7 @@ import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -28,7 +29,7 @@ internal class AvailableToAddDataConverter @Inject constructor( availableNetworks: Set, marketParams: TokenMarketParams, ): AvailableToAddData { - suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { + suspend fun AccountStatus.CryptoPortfolio.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { val currencies = availableNetworks .mapNotNull { network -> createCryptoCurrency( @@ -64,7 +65,7 @@ internal class AvailableToAddDataConverter @Inject constructor( val (_, balance) = entry val wallet = balance.userWallet val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks) - val accounts = balance.accountsBalance.accountStatuses + val accounts = balance.accountsBalance.accountStatuses.filterCryptoPortfolio() val availableToAddAccounts: Map = accounts .mapNotNull { accountStatus -> val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null @@ -103,17 +104,13 @@ internal class AvailableToAddDataConverter @Inject constructor( userWallet: UserWallet, network: TokenMarketInfo.Network, marketParams: TokenMarketParams, - account: Account, + account: Account.CryptoPortfolio, ): CryptoCurrency? { - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, tokenMarketParams = marketParams, network = network, - accountIndex = derivationIndex, + accountIndex = account.derivationIndex, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index 0d733ab09f..ce12bfc05a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TOKEN_ACTIONS_DELAY = 500L @@ -205,7 +205,7 @@ internal class AddToPortfolioModel @Inject constructor( finishFlow() } .catch { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) params.callback.onDismiss() } .launchIn(modelScope) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt index 79ca66cf2b..60acc73b60 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt @@ -29,7 +29,7 @@ import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject @@ -134,7 +134,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId) } .catch { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) params.callback.onDismiss() } .launchIn(modelScope) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index feb3025f69..8b32b2497b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -11,7 +11,7 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.feed.impl.R -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject class CheckCurrencyUnsupportedDelegate @Inject constructor( @@ -29,14 +29,14 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( networkId = rawNetworkId, isMainNetwork = isMainNetwork, ).getOrElse { throwable -> - Timber.e( - throwable, + TangemLogger.e( """ Failed to check currency unsupported state |- User wallet ID: $userWalletId |- Network ID: $rawNetworkId |- Is main network: $isMainNetwork """.trimIndent(), + throwable, ) val message = SnackbarMessage( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt index 50cf8f9881..e1ed13a89e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt @@ -18,6 +18,7 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -192,15 +193,12 @@ internal class MarketsPortfolioDelegate @AssistedInject constructor( }.distinctUntilChanged() private fun AccountStatusList.filterByRawID(): List { - fun AccountStatus.filterByRawID(): List = when (this) { - is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies() - .filter { status -> - val currencyId = status.currency.id.rawCurrencyId ?: return@filter false - getTokenIdIfL2Network(currencyId.value) == currencyRawId.value - } - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return accountStatuses.map { accountStatus -> + fun AccountStatus.CryptoPortfolio.filterByRawID(): List = tokenList.flattenCurrencies() + .filter { status -> + val currencyId = status.currency.id.rawCurrencyId ?: return@filter false + getTokenIdIfL2Network(currencyId.value) == currencyRawId.value + } + return accountStatuses.filterCryptoPortfolio().map { accountStatus -> AccountWithAdded( accountStatus = accountStatus, addedCurrency = accountStatus.filterByRawID(), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt index 1d31f11fe9..7b0b6465db 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @@ -47,7 +46,6 @@ internal class DefaultNewsDetailsComponent( val state by newsDetailsModel.state.collectAsStateWithLifecycle() if (LocalRedesignEnabled.current) { TangemTopBar( - title = resourceReference(R.string.common_news), type = TangemTopBarType.BottomSheet, startContent = { Icon( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt index 3ef3be6ce2..89c501508c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt @@ -16,7 +16,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @@ -44,7 +44,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc tokenId = rawTokenId, tokenSymbol = "", // used for analytics ).getOrElse { - Timber.e("Failed to get market token info") + TangemLogger.e("Failed to get market token info") return@launch } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt index 8409d68360..0419dfe414 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt @@ -9,7 +9,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @@ -25,7 +25,7 @@ internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor( scope.launch { val articleId = extractArticleIdFromUri(deeplinkUri) if (articleId == null) { - Timber.e( + TangemLogger.e( """ Failed to extract article ID from deep link |- Received URI: $deeplinkUri diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 8622c63f4b..71a2894d6b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -325,6 +325,7 @@ internal class EarnModel @Inject constructor( onNetworkFilterClick = ::onNetworkFilterClick, onTypeFilterClick = ::onTypeFilterClick, onScroll = ::onMostlyUsedScrolled, + onSearchBarClicked = params.onMarketOpenClick, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index f074cbdc66..34a71d62a2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -1,8 +1,10 @@ package com.tangem.features.feed.model.earn.state import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer import com.tangem.features.feed.ui.earn.state.* +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -34,6 +36,10 @@ internal class EarnStateController @Inject constructor() { onNetworkFilterClick = {}, onTypeFilterClick = {}, onSliderScroll = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.EMPTY, + onBarClick = {}, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt index d46ac02956..8991f72503 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt @@ -1,12 +1,16 @@ package com.tangem.features.feed.model.earn.state.transformers +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar internal class UpdateEarnUMInitialStateTransformer( private val onBackClick: () -> Unit, private val onNetworkFilterClick: () -> Unit, private val onTypeFilterClick: () -> Unit, private val onScroll: () -> Unit, + private val onSearchBarClicked: () -> Unit, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { @@ -15,6 +19,10 @@ internal class UpdateEarnUMInitialStateTransformer( onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, onSliderScroll = onScroll, + feedListSearchBar = FeedListSearchBar( + onBarClick = onSearchBarClicked, + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 9fb42bb041..3252a220e0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -232,7 +232,13 @@ internal class FeedComponentModel @Inject constructor( return FeedListUM( currentDate = getCurrentDate(), feedListSearchBar = FeedListSearchBar( - placeholderText = resourceReference(R.string.markets_search_header_title), + placeholderText = resourceReference( + id = if (feedFeatureToggle.isEarnBlockEnabled) { + R.string.markets_search_title_placeholder + } else { + R.string.markets_search_header_title + }, + ), onBarClick = { analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked()) params.feedClickIntents.onMarketOpenClick(null) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index e155e80515..f362ecfdab 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -53,6 +53,7 @@ import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoCo import com.tangem.features.feed.model.market.details.formatter.* import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater import com.tangem.features.feed.model.market.details.state.TokenNetworksState +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.lib.crypto.BlockchainUtils @@ -258,6 +259,10 @@ internal class MarketsTokenDetailsModel @Inject constructor( onScroll = {}, ), onShareClick = ::onShareClick, + feedListSearchBar = FeedListSearchBar( + onBarClick = { params.onMarketOpenClick() }, + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + ), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt index 03579058d8..d3ba6deb04 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt @@ -6,10 +6,10 @@ import com.tangem.domain.markets.TokenMarketUpdateRequest import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchUpdateResult import com.tangem.pagination.PaginationStatus -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal fun logStatus(tag: String, status: PaginationStatus>) { - Timber.tag(tag).d( + TangemLogger.withTag(tag).d( """ Status $status @@ -19,19 +19,19 @@ internal fun logStatus(tag: String, status: PaginationStatus>) internal fun logAction(tag: String, action: BatchAction) { when (action) { - is BatchAction.Reload -> Timber.tag(tag).d( + is BatchAction.Reload -> TangemLogger.withTag(tag).d( """ Reload = ${action.requestParams} """.trimIndent(), ) - is BatchAction.UpdateBatches -> Timber.tag(tag).d( + is BatchAction.UpdateBatches -> TangemLogger.withTag(tag).d( """ To update: keys: ${action.keys.toList()} updateType: ${action.updateRequest.javaClass.simpleName} """.trimIndent(), ) - else -> Timber.tag(tag).d( + else -> TangemLogger.withTag(tag).d( """ $action """.trimIndent(), @@ -48,7 +48,7 @@ internal fun logUpdateResults( is BatchUpdateResult.Error -> s.throwable.toString() } - Timber.tag(tag).d( + TangemLogger.withTag(tag).d( """ updateResults request: ${updateResult.first} diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt index 63cc26391a..bdd662e7f2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt @@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.Locale import javax.inject.Inject @@ -134,7 +134,7 @@ internal class NewsDetailsModel @Inject constructor( modelScope.launch { toggleArticleLikedUseCase .toggleLiked(articleId) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId)) } } @@ -231,7 +231,7 @@ internal class NewsDetailsModel @Inject constructor( } // global request executing error newsId < 0 -> { - Timber.e(error) + TangemLogger.e("Error", error) } else -> { val (code, message) = when (error) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt new file mode 100644 index 0000000000..34aa87a805 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt @@ -0,0 +1,136 @@ +package com.tangem.features.feed.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar + +@Composable +internal fun FeedSearchBar( + isSearchBarClickable: Boolean, + feedListSearchBar: FeedListSearchBar, + modifier: Modifier = Modifier, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + if (LocalRedesignEnabled.current) { + FeedSearchBarV2( + isSearchBarClickable = isSearchBarClickable, + feedListSearchBar = feedListSearchBar, + modifier = modifier, + startContent = startContent, + endContent = endContent, + ) + } else { + FeedSearchBarV1( + isSearchBarClickable = isSearchBarClickable, + feedListSearchBar = feedListSearchBar, + modifier = modifier, + ) + } +} + +@Composable +private fun FeedSearchBarV1( + isSearchBarClickable: Boolean, + feedListSearchBar: FeedListSearchBar, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(36.dp)) + .background(color = TangemTheme.colors.field.focused) + .conditional(condition = isSearchBarClickable) { + clickable(onClick = feedListSearchBar.onBarClick) + } + .padding(14.dp), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + + SpacerW(14.dp) + + Text( + text = feedListSearchBar.placeholderText.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun FeedSearchBarV2( + isSearchBarClickable: Boolean, + feedListSearchBar: FeedListSearchBar, + modifier: Modifier = Modifier, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + TangemTopBar( + modifier = modifier, + startContent = startContent, + endContent = endContent, + type = TangemTopBarType.BottomSheet, + reserveSlotSpace = false, + content = { + Row( + modifier = Modifier + .weight(1f) + .padding( + start = if (startContent != null) TangemTheme.dimens2.x3 else 0.dp, + end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp, + ) + .clip(CircleShape) + .background(color = TangemTheme.colors2.field.backgroundDefault) + .conditional(condition = isSearchBarClickable) { + clickable(onClick = feedListSearchBar.onBarClick) + } + .padding(TangemTheme.dimens2.x3), + horizontalArrangement = Arrangement.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x5), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24), + tint = TangemTheme.colors2.markers.iconGray, + contentDescription = null, + ) + + SpacerW(TangemTheme.dimens2.x1) + + Text( + text = feedListSearchBar.placeholderText.resolveReference(), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 5f9756b351..86ca75fa5b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.extensions.stringReference @@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.* import com.tangem.features.feed.ui.earn.components.* import com.tangem.features.feed.ui.earn.state.* +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.collections.immutable.persistentListOf private const val EARN_LOAD_MORE_BUFFER = 3 @@ -655,6 +657,10 @@ private fun previewEarnUM( onNetworkFilterClick = {}, onTypeFilterClick = {}, onSliderScroll = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.Str("Search tokens & news"), + onBarClick = {}, + ), ) private const val PLACEHOLDER_ITEMS_COUNT = 8 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index 974b5a71b2..fd8611ff2b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar @Immutable internal data class EarnUM( @@ -11,4 +12,5 @@ internal data class EarnUM( val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, val onSliderScroll: () -> Unit, + val feedListSearchBar: FeedListSearchBar, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index e8267f9b85..26f97c3c93 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -4,34 +4,22 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.extensions.conditional -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.feed.components.* import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState import com.tangem.features.feed.ui.feed.state.FeedListSearchBar @@ -50,8 +38,6 @@ internal fun FeedListHeader( feedListSearchBar = feedListSearchBar, modifier = modifier .drawBehind { drawRect(background) } - .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) .testTag(SEARCH_BAR), ) } @@ -95,39 +81,6 @@ internal fun FeedList( } } -@Composable -private fun FeedSearchBar( - isSearchBarClickable: Boolean, - feedListSearchBar: FeedListSearchBar, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(36.dp)) - .background(color = TangemTheme.colors.field.focused) - .conditional(condition = isSearchBarClickable) { - clickable(onClick = feedListSearchBar.onBarClick) - } - .padding(14.dp), - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - - SpacerW(14.dp) - - Text( - text = feedListSearchBar.placeholderText.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - @Composable private fun FeedListContent( state: FeedListUM, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt index a1183e5467..32a6229d2e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -23,13 +24,16 @@ import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @Composable -internal fun Header( +internal fun ColumnScope.Header( onSeeAllClick: () -> Unit, isLoading: Boolean, shouldShowSeeAll: Boolean, title: @Composable () -> Unit, ) { val isRedesignEnabled = LocalRedesignEnabled.current + if (isRedesignEnabled) { + SpacerH(20.dp) + } AnimatedContent(isLoading) { animatedState -> Row( modifier = Modifier diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt index de6399ee68..b38b335ae1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt @@ -9,10 +9,20 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @Composable internal fun DateBlock(currentDate: String) { + if (LocalRedesignEnabled.current) { + DateBlockV2(currentDate) + } else { + DateBlockV1(currentDate) + } +} + +@Composable +private fun DateBlockV1(currentDate: String) { SpacerH(20.dp) Text( modifier = Modifier @@ -30,4 +40,26 @@ internal fun DateBlock(currentDate: String) { style = TangemTheme.typography.h2, color = TangemTheme.colors.text.tertiary, ) +} + +@Composable +private fun DateBlockV2(currentDate: String) { + SpacerH(TangemTheme.dimens2.x1) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + text = stringResourceSafe(R.string.feed_market_and_news), + style = TangemTheme.typography2.headingRegular28, + color = TangemTheme.colors2.text.neutral.primary, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + text = currentDate, + style = TangemTheme.typography2.headingRegular28, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + SpacerH(TangemTheme.dimens2.x6) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt index b24607dc57..832e2e4ea0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -71,7 +71,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis isLoading = currentChart is MarketChartUM.Loading, ) - SpacerH(12.dp) + SpacerH(if (isRedesignEnabled) 20.dp else 12.dp) Charts( onItemClick = feedListCallbacks.onMarketItemClick, @@ -89,7 +89,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis @Suppress("LongMethod") @Composable -internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { +internal fun ColumnScope.MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { val isRedesignEnabled = LocalRedesignEnabled.current val onSeeAllClick by rememberUpdatedState { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt index ed7261d399..65ce49e61e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt @@ -5,8 +5,10 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.detailed.state.* import kotlinx.collections.immutable.persistentListOf @@ -50,6 +52,10 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.Str("Search tokens & news"), + onBarClick = {}, + ), ) val contentState = MarketsTokenDetailsUM( @@ -144,5 +150,9 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.Str("Search tokens & news"), + onBarClick = {}, + ), ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index 5f21370028..c6e0bde66a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -29,6 +30,7 @@ internal data class MarketsTokenDetailsUM( val onShouldShowPriceSubtitleChange: (Boolean) -> Unit, val relatedNews: RelatedNews, val onShareClick: () -> Unit, + val feedListSearchBar: FeedListSearchBar, ) { data class ChartState( diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index bd620457d9..afa7941725 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -22,7 +22,8 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) - + implementation(projects.core.utils) + /** Common */ implementation(projects.common.routing) @@ -63,7 +64,6 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 7cb91f3880..97c6d2a93c 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -17,9 +17,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.analytics.ParamCardCurrencyConverter @@ -48,7 +46,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.Locale import javax.inject.Inject @@ -196,7 +194,7 @@ internal class HomeModel @Inject constructor( val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return } @@ -205,11 +203,11 @@ internal class HomeModel @Inject constructor( userWallet = userWallet, analyticsSource = AnalyticsParam.ScreensSources.Intro, ).fold( - ifLeft = { + ifLeft = { error -> delay(HIDE_PROGRESS_DELAY) setLoading(false) - when (it) { - is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + when (error) { + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error") is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) } }, @@ -245,17 +243,12 @@ internal class HomeModel @Inject constructor( private fun handleScanError(error: TangemError) { when (error) { is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() - is TangemSdkError -> Timber.e(error, "Scan error occurred") - else -> Timber.e(error, "Error happened") + is TangemSdkError -> TangemLogger.e("Scan error occurred", error) + else -> TangemLogger.e("Error happened", error) } } private fun handleNfcFeatureUnavailable() { - uiMessageSender.send( - message = DialogMessage( - message = resourceReference(R.string.nfc_error_unavailable), - title = resourceReference(id = R.string.common_error), - ), - ) + uiMessageSender.send(Dialogs.nfcFeatureUnavailable()) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 380b51678e..37c0ca6773 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -72,7 +72,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 1a2cb122b6..f23c52ad2f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -26,7 +26,7 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped @@ -53,7 +53,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) { if (userWalletExists(attemptRequest.hotWalletId).not()) { - Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") + TangemLogger.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") result.value = HotWalletPasswordRequester.Result.Dismiss return } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index e2d4d75a14..eb4f297ea2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.utils.logging.TangemLogger import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent @@ -28,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -96,10 +96,10 @@ internal class AddExistingWalletImportModel @Inject constructor( val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true)) - .onLeft { + .onLeft { error -> setImportProgress(false) - when (it) { - is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + when (error) { + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error") is SaveWalletError.WalletAlreadySaved -> { uiMessageSender.send( SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), @@ -129,8 +129,8 @@ internal class AddExistingWalletImportModel @Inject constructor( ) params.callbacks.onWalletImported(userWallet.walletId) } - }.onFailure { - Timber.e(it) + }.onFailure { throwable -> + TangemLogger.e("Error", throwable) setImportProgress(false) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index 49c8bc6351..c11626ce87 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -12,9 +12,9 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val HIDE_PROGRESS_DELAY = 400L @@ -129,7 +129,7 @@ internal class CreateHardwareWalletModel @Inject constructor( val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return } @@ -142,7 +142,7 @@ internal class CreateHardwareWalletModel @Inject constructor( delay(HIDE_PROGRESS_DELAY) setLoading(false) when (saveWalletError) { - is SaveWalletError.DataError -> Timber.e(saveWalletError.toString(), "Unable to save user wallet") + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $saveWalletError") is SaveWalletError.WalletAlreadySaved -> handleAlreadySavedCard( saveWalletError.messageId, walletId = userWallet.walletId, @@ -164,18 +164,13 @@ internal class CreateHardwareWalletModel @Inject constructor( private fun handleScanError(error: TangemError) { when (error) { is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() - is TangemSdkError -> Timber.e(error, "Scan error occurred") - else -> Timber.e(error, "Error happened") + is TangemSdkError -> TangemLogger.e("Scan error occurred", error) + else -> TangemLogger.e("Error happened", error) } } private fun handleNfcFeatureUnavailable() { - uiMessageSender.send( - message = DialogMessage( - message = resourceReference(R.string.nfc_error_unavailable), - title = resourceReference(id = R.string.common_error), - ), - ) + uiMessageSender.send(Dialogs.nfcFeatureUnavailable()) } private suspend fun handleAlreadySavedCard(messageId: Int, walletId: UserWalletId, scanResponse: ScanResponse) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index dad097a1fb..a5c4e17316 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -112,7 +112,7 @@ internal class CreateMobileWalletModel @Inject constructor( router.replaceAll(AppRoute.Wallet) }.onFailure { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) uiState.update { it.copy(createButtonLoading = false) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt index a4d9c89ffb..872ba5c072 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.forgetwallet import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -80,8 +80,8 @@ internal class ForgetWalletModel @Inject constructor( private fun forgetWallet() { modelScope.launch { val hasUserWallets = deleteWalletUseCase(params.userWalletId) - .getOrElse { - Timber.e("Unable to delete wallet: $it") + .getOrElse { error -> + TangemLogger.e("Unable to delete wallet: $error") uiMessageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt index b68069c4fe..3006348ca2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.manualbackup.check.model import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,15 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -import kotlin.Boolean -import kotlin.Int -import kotlin.String -import kotlin.Suppress -import kotlin.collections.List -import kotlin.collections.all -import kotlin.collections.map @Stable @ModelScoped @@ -63,7 +56,7 @@ internal class ManualBackupCheckModel @Inject constructor( } } }.onFailure { - Timber.e(it) + TangemLogger.e("Error", it) } } } @@ -114,8 +107,8 @@ internal class ManualBackupCheckModel @Inject constructor( uiState.update { it.copy(completeButtonProgress = false) } - }.onFailure { - Timber.e(it) + }.onFailure { throwable -> + TangemLogger.e("Error", throwable) uiState.update { it.copy(completeButtonProgress = false) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt index abb3ea0654..f4c716ed2b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -58,7 +58,7 @@ internal class ManualBackupPhraseModel @Inject constructor( } } }.onFailure { - Timber.e(it) + TangemLogger.e("Error", it) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt index 808f1df0b6..eb373c4429 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -53,7 +53,7 @@ internal class ViewPhraseModel @Inject constructor( modelScope.launch { val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId) - .getOrElse { error -> Timber.e(error); throw error } + .getOrElse { error -> TangemLogger.e("Error", error); throw error } .mnemonic.mnemonicComponents uiState.update { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 7c284f9cef..32986a6549 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -21,7 +21,7 @@ import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -71,7 +71,7 @@ internal class WalletBackupModel @Inject constructor( .onEach { either -> either.fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) { @@ -128,13 +128,13 @@ internal class WalletBackupModel @Inject constructor( getUserWalletUseCase.invoke(params.userWalletId) .fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> when (userWallet) { is UserWallet.Cold -> { val userWalletId = userWallet.walletId - Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId") + TangemLogger.e("Unexpected cold wallet when request seed phrase: $userWalletId") } is UserWallet.Hot -> showSeedPhrase(userWallet) } @@ -155,7 +155,7 @@ internal class WalletBackupModel @Inject constructor( unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId) .fold( ifLeft = { - Timber.e("Error while export seed phrase: $it") + TangemLogger.e("Error while export seed phrase: $it") }, ifRight = { seedPhrasePrivateInfo -> router.push(AppRoute.ViewPhrase(params.userWalletId)) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt index c432ac4cc1..d7a12e7a10 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt @@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -147,7 +147,7 @@ internal class WalletHardwareBackupModel @Inject constructor( -> modelScope.launch { unlockHotWalletContextualUseCase.invoke(hotWalletId) .onLeft { - Timber.e(it, "Unable to unlock wallet with id ${params.userWalletId}") + TangemLogger.e("Unable to unlock wallet with id ${params.userWalletId}", it) } .onRight { router.push(AppRoute.UpgradeWallet(userWalletId = params.userWalletId)) diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts index 1bf8fc66f2..f51dea8c7d 100644 --- a/features/kyc/impl/build.gradle.kts +++ b/features/kyc/impl/build.gradle.kts @@ -49,7 +49,6 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) implementation(deps.sumsub.sdk) implementation(deps.arrow.core) diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt index f729fe489c..d181f971dc 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -13,7 +13,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -40,7 +40,7 @@ class DefaultKycModel @Inject constructor( tangemPayEligibilityManager.reset() } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) } } } diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 4a0329dcad..867d3f035c 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.configToggles) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.common.routing) implementation(projects.common.ui) @@ -67,6 +68,5 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.reKotlin) // need for legacy onboarding } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 43fbc9472f..1d6522b7b1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -41,7 +41,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -243,7 +243,7 @@ internal class ChooseManagedTokensModel @Inject constructor( } is PaginationStatus.Paginating -> { (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> - Timber.e(fetchError.throwable) + TangemLogger.e("Error", fetchError.throwable) } state.copy( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 5418e2b7b1..e62d320665 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -36,7 +37,6 @@ import kotlinx.collections.immutable.mutate import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject // TODO: Divide to sub-components: [REDACTED_JIRA] @@ -239,7 +239,7 @@ internal class CustomTokenFormModel @Inject constructor( } private fun showErrorDialog(throwable: Throwable) { - Timber.e(throwable) + TangemLogger.e("Error", throwable) val message = when (throwable) { is TangemSdkError -> resourceReference( R.string.generic_error_code, @@ -279,7 +279,7 @@ internal class CustomTokenFormModel @Inject constructor( stateAcc // No need to display error } is CustomTokenFormValidationException.DataError -> { - Timber.e(exception.cause, "Unable to validate custom currency") + TangemLogger.e("Unable to validate custom currency", exception.cause) stateAcc } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 960347fc65..2c10b94d72 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -38,7 +38,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -248,7 +248,7 @@ internal class ManageTokensModel @Inject constructor( } is PaginationStatus.Paginating -> { (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> - Timber.e(fetchError.throwable) + TangemLogger.e("Error", fetchError.throwable) } state.copySealed( @@ -339,7 +339,7 @@ internal class ManageTokensModel @Inject constructor( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e(throwable, "Failed to save changes") + TangemLogger.e("Failed to save changes", throwable) return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 02a234043f..4010fb57e9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -33,7 +33,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -167,7 +167,7 @@ internal class OnboardingManageTokensModel @Inject constructor( } is PaginationStatus.Paginating -> { (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> - Timber.e(fetchError.throwable) + TangemLogger.e("Error", fetchError.throwable) } state.copy( @@ -261,7 +261,7 @@ internal class OnboardingManageTokensModel @Inject constructor( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e(throwable, "Failed to save changes") + TangemLogger.e("Failed to save changes", throwable) return@resource } @@ -286,7 +286,7 @@ internal class OnboardingManageTokensModel @Inject constructor( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e(throwable, "Failed to save changes") + TangemLogger.e("Failed to save changes", throwable) return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index 698d65bd5b..d62b31c3e5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -16,7 +16,7 @@ import com.tangem.utils.coroutines.saveInAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class CustomCurrencyValidator( private val userWalletId: UserWalletId, @@ -103,7 +103,7 @@ internal class CustomCurrencyValidator( ).getOrElse { e -> when (e) { is FindTokenException.DataError -> { - Timber.e(e.cause, "Unable to find custom currency") + TangemLogger.e("Unable to find custom currency", e.cause) updateStatus(Status.UnexpectedException(e.cause)) return } @@ -135,7 +135,7 @@ internal class CustomCurrencyValidator( ).getOrElse { e -> val newStatus = when (e) { is FindTokenException.DataError -> { - Timber.e(e.cause, "Unable to find custom currency") + TangemLogger.e("Unable to find custom currency", e.cause) Status.UnexpectedException(e.cause) } is FindTokenException.NotFound -> { @@ -161,7 +161,7 @@ internal class CustomCurrencyValidator( derivationPath = derivationPath, formValues = validatedForm, ).getOrElse { e -> - Timber.e(e, "Unable to create custom currency") + TangemLogger.e("Unable to create custom currency", e) updateStatus(Status.UnexpectedException(e)) return } @@ -181,7 +181,7 @@ internal class CustomCurrencyValidator( is CryptoCurrency.Token -> currency.contractAddress }, ).getOrElse { e -> - Timber.e(e, "Unable to check if currency is already added") + TangemLogger.e("Unable to check if currency is already added", e) updateStatus(Status.UnexpectedException(e)) return diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt index ea253fd2d2..96cc6f1e8a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.right +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.account.producer.SingleAccountListProducer @@ -21,7 +22,6 @@ import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( @Assisted private val userWalletId: UserWalletId, @@ -84,14 +84,14 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( val blockchain = Blockchain.fromNetworkId(networkId = currency.network.backendId) if (blockchain == null) { val exception = IllegalStateException("Token has unknown networkId: ${currency.id}") - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) } val derivationPathValue = currency.network.derivationPath.value if (derivationPathValue == null) { val exception = IllegalStateException("Token has no derivation path: ${currency.id}") - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) } @@ -99,10 +99,9 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( val index = accountNodeRecognizer.recognize(derivationPathValue)?.toInt() if (index == null) { - Timber.e( - "%s%s", - "Unable to determine account index for derivation path: $derivationPathValue. ", - "Use main account index instead.", + TangemLogger.e( + "Unable to determine account index for derivation path: $derivationPathValue. " + + "Use main account index instead.", ) DerivationIndex.Main.value } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 6265318696..7020d8874f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList", "LargeClass") internal class ManageTokensListManager @AssistedInject constructor( @@ -222,7 +222,7 @@ internal class ManageTokensListManager @AssistedInject constructor( scope.launch { useCasesFacade.removeCustomCurrencyUseCase(currency) .onRight { reload() } - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } } } @@ -266,13 +266,13 @@ internal class ManageTokensListManager @AssistedInject constructor( tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value, tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e( - throwable, + TangemLogger.e( """ Failed to check linked tokens |- Mode: $mode |- Network ID: ${network.id} """.trimIndent(), + throwable, ) val message = SnackbarMessage( @@ -292,13 +292,13 @@ internal class ManageTokensListManager @AssistedInject constructor( return useCasesFacade.checkCurrencyUnsupportedUseCase( sourceNetwork = sourceNetwork, ).getOrElse { throwable -> - Timber.e( - throwable, + TangemLogger.e( """ Failed to check currency unsupported state |- Mode: $mode |- Source Network: $sourceNetwork """.trimIndent(), + throwable, ) val message = SnackbarMessage( diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 86304b9fd5..bc76460d54 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -68,7 +68,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.decompose.ext.compose) /* Core */ @@ -78,6 +77,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) + implementation(projects.core.utils) /* Common */ implementation(projects.common.ui) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 693be634b3..7db2bdc36d 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -73,7 +73,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 2f6f77b6c7..13fc9173de 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -21,7 +21,7 @@ import com.tangem.features.nft.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -136,7 +136,7 @@ internal class NFTCollectionsModel @Inject constructor( _state.update { ChangeRefreshingStateTransformer(true).transform(it) } try { refreshAllNFTUseCase(params.userWalletId) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } } finally { _state.update { ChangeRefreshingStateTransformer(false).transform(it) } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index e47a5fca31..4a91c998a3 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -115,7 +115,7 @@ internal class NFTDetailsModel @Inject constructor( getNFTPriceUseCase(params.userWalletId, params.nftAsset) .fold( ifLeft = { - Timber.w(it) + TangemLogger.w("Error", it) }, ifRight = { quoteFlow -> quoteFlow diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index a992e7daea..917e123120 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -81,7 +81,6 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) implementation(tangemDeps.card.core) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index b1893158d7..924382dc55 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.twin.impl.model +import com.tangem.utils.logging.TangemLogger import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -47,7 +48,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -296,7 +296,7 @@ internal class OnboardingTwinModel @Inject constructor( private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope { val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return@coroutineScope } @@ -305,8 +305,8 @@ internal class OnboardingTwinModel @Inject constructor( userWallet = userWallet, canOverride = true, analyticsSource = AnalyticsParam.ScreensSources.Onboarding, - ).onLeft { - Timber.e("Unable to save user wallet: $it") + ).onLeft { error -> + TangemLogger.e("Unable to save user wallet: $error") setLoading(false) return@coroutineScope } @@ -326,7 +326,7 @@ internal class OnboardingTwinModel @Inject constructor( modelScope.launch { val userWallet = coldUserWalletBuilderFactory.create(params.scanResponse).build() ?: run { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return@launch } @@ -335,8 +335,8 @@ internal class OnboardingTwinModel @Inject constructor( userWallet = userWallet, canOverride = true, analyticsSource = AnalyticsParam.ScreensSources.Onboarding, - ).onLeft { - Timber.e("Unable to save user wallet: $it") + ).onLeft { error -> + TangemLogger.e("Unable to save user wallet: $error") setLoading(false) return@launch } diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index fcab39bc50..4c6b3a90a3 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -82,5 +82,4 @@ dependencies { implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) implementation(deps.reKotlin) - implementation(deps.timber) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index 5b0c5748d3..89dd1d8ecf 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class AllOffersModel @Inject constructor( @@ -127,7 +127,7 @@ internal class AllOffersModel @Inject constructor( } private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) + TangemLogger.e(onrampError.toString()) state.update { stateFactory.getOnrampErrorState(onrampError) } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt index 5633cf6424..4210dfcdc6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultBuyDeepLinkHandler @AssistedInject constructor( router: AppRouter, @@ -16,7 +16,7 @@ internal class DefaultBuyDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> router.push(AppRoute.BuyCrypto(userWallet.walletId)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt index 035c32ef13..ff2e4d0a3e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt @@ -8,7 +8,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultOnrampDeepLinkHandler @AssistedInject constructor( @Assisted scope: CoroutineScope, @@ -36,7 +36,7 @@ internal class DefaultOnrampDeepLinkHandler @AssistedInject constructor( } } else -> { - Timber.e( + TangemLogger.e( """ Invalid parameters for ONRAMP deeplink |- Params: $queryParams diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt index ee38f9501a..eb25b50f21 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultSellDeepLinkHandler @AssistedInject constructor( router: AppRouter, @@ -16,7 +16,7 @@ internal class DefaultSellDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> router.push(AppRoute.SellCrypto(userWallet.walletId)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt index 7fff8ef48c..584aadf822 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultSwapDeepLinkHandler @AssistedInject constructor( router: AppRouter, @@ -16,7 +16,7 @@ internal class DefaultSwapDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> router.push(AppRoute.SwapCrypto(userWallet.walletId)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index bf19ec5d37..cf1badae14 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -41,7 +41,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -187,7 +187,7 @@ internal class HotCryptoModel @Inject constructor( channel.close() } .catch { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) closeNavigationFlow() } .launchIn(modelScope) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index 9863a66e16..cb33ca61de 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -56,13 +56,13 @@ internal class OnrampAddToPortfolioModel @Inject constructor( private fun isTangemIconVisible(): Boolean { return getUserWalletUseCase(params.userWalletId) - .onLeft { Timber.e("Unable to get wallet by id [${params.userWalletId}]: $it") } + .onLeft { TangemLogger.e("Unable to get wallet by id [${params.userWalletId}]: $it") } .fold(ifLeft = { false }, ifRight = { it is UserWallet.Cold }) } private fun getUserWalletName(): String { return getUserWalletUseCase(params.userWalletId) - .onLeft { Timber.e("Unable to get wallet name by id [${params.userWalletId}]: $it") } + .onLeft { TangemLogger.e("Unable to get wallet name by id [${params.userWalletId}]: $it") } .fold(ifLeft = { "" }, ifRight = UserWallet::name) } @@ -74,7 +74,7 @@ internal class OnrampAddToPortfolioModel @Inject constructor( manageCryptoCurrenciesUseCase(accountId = accountId, add = params.cryptoCurrency) .onRight { params.onSuccessAdding(params.cryptoCurrency.id) } .onLeft { throwable -> - Timber.e("Failed to add crypto currency: $throwable") + TangemLogger.e("Failed to add crypto currency: $throwable") changeAddButtonProgressStatus(isProgress = false) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index b72f54919d..63d3cce362 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -29,7 +29,7 @@ import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -351,7 +351,7 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) + TangemLogger.e(onrampError.toString()) state.update { stateFactory.getOnrampErrorState(onrampError) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index 95cab56383..ec8ba318be 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -116,7 +116,7 @@ internal class SelectProviderModel @Inject constructor( } .onLeft { error -> sendOnrampErrorEvent(error) - Timber.e(error.toString()) + TangemLogger.e(error.toString()) } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index a550929c72..1540f81f69 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -133,7 +133,7 @@ internal class OnrampRedirectModel @Inject constructor( } private fun handleError(error: OnrampError) { - Timber.e(error.toString()) + TangemLogger.e(error.toString()) analyticsEventHandler.sendOnrampErrorEvent( error = error, tokenSymbol = params.cryptoCurrency.symbol, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 3fc7167dfc..dbae3fb93d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -89,12 +89,12 @@ internal class OnrampSuccessComponentModel @Inject constructor( getOnrampTransactionUseCase(txId = params.txId) .fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) showErrorAlert(error) }, ifRight = { transaction -> userWallet = getUserWalletUseCase(transaction.userWalletId).getOrElse { - Timber.e("UserWallet found") + TangemLogger.e("UserWallet not found") // this case should never happened showErrorAlert(OnrampError.DomainError("UserWallet not found")) return@launch @@ -104,7 +104,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( ?.getCryptoCurrency(currencyIdValue = transaction.toCurrencyId)?.getOrNull() .toOption() .getOrElse { - Timber.e("Crypto currency not found") + TangemLogger.e("Crypto currency not found") showErrorAlert(OnrampError.DomainError(null)) return@launch } @@ -145,7 +145,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( providerName = transaction.providerName, paymentMethod = transaction.paymentMethod, ) - Timber.e(error.toString()) + TangemLogger.e(error.toString()) showErrorAlert(error) }, ifRight = { status -> diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt index 616f0761c4..8adcb4725d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt @@ -5,7 +5,7 @@ import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -26,12 +26,12 @@ internal class SwapSelectTokensController @Inject constructor() { ) fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) { - Timber.d("Applying non-name transformation") + TangemLogger.d("Applying non-name transformation") state.update(transform) } fun update(transformer: SwapSelectTokensUMTransformer) { - Timber.d("Applying ${transformer::class.simpleName ?: "null"}") + TangemLogger.d("Applying ${transformer::class.simpleName ?: "null"}") state.update(transformer::transform) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt index 5fb7287648..d2cb9c3ef3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -36,17 +36,17 @@ internal class TokenListUMController @Inject constructor() { ) fun update(transform: (TokenListUM) -> TokenListUM) { - Timber.d("Applying non-name transformation") + TangemLogger.d("Applying non-name transformation") state.update(transform) } fun update(transformer: TokenListUMTransformer) { - Timber.d("Applying ${transformer::class.simpleName ?: "unknown"}") + TangemLogger.d("Applying ${transformer::class.simpleName ?: "unknown"}") state.update(transformer::transform) } fun update(transformer: SearchBarUMTransformer) { - Timber.d("Applying ${transformer::class.simpleName ?: "unknown"}") + TangemLogger.d("Applying ${transformer::class.simpleName ?: "unknown"}") state.update { prevState -> prevState.copy( searchBarUM = transformer.transform(prevState.searchBarUM), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index ddc4c9751a..5b62caa0f6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry @@ -105,7 +106,7 @@ internal class OnrampTokenListModel @Inject constructor( updateTokenListUM( SetLoadingAccountTokenListTransformer( appCurrency = appCurrency, - accountList = accountList.accountStatuses.toList(), + accountList = accountList.accountStatuses.filterCryptoPortfolio().toList(), isAccountsMode = isAccountsMode, ), ) @@ -237,6 +238,7 @@ internal class OnrampTokenListModel @Inject constructor( private fun AccountStatusList.filterAccountsByQuery( query: String, ): Map> = accountStatuses.asSequence() + .filterCryptoPortfolio() .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { diff --git a/features/promo-banners/impl/build.gradle.kts b/features/promo-banners/impl/build.gradle.kts index 0bb6f452b2..d90d0e2f70 100644 --- a/features/promo-banners/impl/build.gradle.kts +++ b/features/promo-banners/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { /** Core */ implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.analytics) implementation(projects.core.analytics.models) @@ -27,9 +28,6 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.configToggles) - /** Common */ - implementation(projects.common.routing) - /** Compose */ implementation(deps.compose.foundation) implementation(deps.compose.ui) @@ -38,7 +36,6 @@ dependencies { /** Other */ implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt index 229e06fcdb..d6ca0f6001 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt @@ -8,34 +8,51 @@ internal sealed class PromoBannerAnalyticsEvent( ) : AnalyticsEvent(category = "Promo Banner", event = event, params = params) { data class Shown( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Shown", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) data class CarouselScrolled( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Carousel Scrolled", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) data class Clicked( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Button Clicked", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) data class Dismissed( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Dismissed", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) + + private companion object { + const val PARAM_DISPLAY_ID = "Display Id" + const val PARAM_PLACEHOLDER = "Placeholder" + } } \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt index 13e19d5361..ef32e2291c 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt @@ -25,7 +25,7 @@ internal class PromoBannerDisplayDTOConverter : Converter PromoBannerPriority.IMPORTANT + "TOP" -> PromoBannerPriority.TOP "HIGH" -> PromoBannerPriority.HIGH "MEDIUM" -> PromoBannerPriority.MEDIUM "LOW" -> PromoBannerPriority.LOW diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt index 0e81007163..91c29f151a 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt @@ -10,7 +10,7 @@ internal class PromoBannerDisplayToNotificationConverter { fun convert( banner: PromoBannerDisplay, onDeeplinkClick: (String?) -> Unit, - onDismiss: (String) -> Unit, + onDismiss: (Int) -> Unit, ): PromoBannerNotificationUM { return PromoBannerNotificationUM( displayId = banner.id, diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt index 6f96a1cf00..15c278cee2 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt @@ -1,7 +1,7 @@ package com.tangem.features.promobanners.impl.model internal data class PromoBannerDisplay( - val id: String, + val id: Int, val placeholder: String, val priority: PromoBannerPriority, val title: String, diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt index 1d244d419c..6003b4e54f 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt @@ -3,6 +3,6 @@ package com.tangem.features.promobanners.impl.model import com.tangem.core.ui.components.notifications.NotificationConfig internal data class PromoBannerNotificationUM( - val displayId: String, + val displayId: Int, val config: NotificationConfig, ) \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt index 959f3c3b44..bdb0403226 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt @@ -1,7 +1,7 @@ package com.tangem.features.promobanners.impl.model internal enum class PromoBannerPriority(val order: Int) { - IMPORTANT(order = 0), + TOP(order = 0), HIGH(order = 1), MEDIUM(order = 2), LOW(order = 3), diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt index 7a9a54c6db..f4f2d505e5 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt @@ -1,6 +1,6 @@ package com.tangem.features.promobanners.impl.model -import com.tangem.common.routing.LinkHandler +import com.tangem.core.navigation.deeplink.DeeplinkLauncher import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -12,10 +12,11 @@ import com.tangem.features.promobanners.impl.converters.PromoBannerDisplayToNoti import com.tangem.features.promobanners.impl.repository.PromoBannersRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.collections.immutable.persistentListOf +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.util.Locale import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -25,7 +26,7 @@ internal class PromoBannersBlockModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val repository: PromoBannersRepository, - private val linkHandler: LinkHandler, + private val deeplinkLauncher: DeeplinkLauncher, private val analyticsEventHandler: AnalyticsEventHandler, private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { @@ -34,11 +35,12 @@ internal class PromoBannersBlockModel @Inject constructor( private val converter = PromoBannerDisplayToNotificationConverter() private val placeholder: String = params.placeholder.name.lowercase() - private val shownBannerIds: MutableSet = ConcurrentHashMap.newKeySet() + private val shownBannerIds: MutableSet = ConcurrentHashMap.newKeySet() private var wasCarouselScrolled = false + private val savedDisplayIdByWalletId: MutableMap = mutableMapOf() val uiState: StateFlow - field = MutableStateFlow(PromoBannersBlockUM()) + field = MutableStateFlow(getInitialState()) init { subscribeOnSelectedWallet() @@ -59,46 +61,67 @@ internal class PromoBannersBlockModel @Inject constructor( } private suspend fun loadBanners(walletId: String) { - val locale = Locale.getDefault().language + val languageISOCode = Locale.getDefault().language runSuspendCatching { - repository.getBanners(walletId, params.placeholder, locale) + repository.getBanners(walletId, params.placeholder, languageISOCode) }.onSuccess { banners -> + val bannerUMs = banners.map { banner -> + converter.convert( + banner = banner, + onDeeplinkClick = { deeplink -> onButtonClick(banner.id, deeplink) }, + onDismiss = { displayId -> onBannerDismiss(walletId, displayId) }, + ) + }.toImmutableList() + + val savedDisplayId = savedDisplayIdByWalletId[walletId] + val initialPage = if (savedDisplayId != null) { + bannerUMs.indexOfFirst { it.displayId == savedDisplayId }.coerceAtLeast(0) + } else { + 0 + } + uiState.value = PromoBannersBlockUM( - banners = banners.map { banner -> - converter.convert( - banner = banner, - onDeeplinkClick = { deeplink -> onButtonClick(banner.id, deeplink) }, - onDismiss = { displayId -> onBannerDismiss(walletId, displayId) }, - ) - }.toImmutableList(), + userWalletId = walletId, + initialPage = initialPage, + banners = bannerUMs, onBannerShown = ::onBannerShown, onCarouselScrolled = ::onCarouselScrolled, + onPageChanged = { displayId -> savedDisplayIdByWalletId[walletId] = displayId }, ) }.onFailure { error -> - Timber.w(error, "Failed to load promo banners") + TangemLogger.w("Failed to load promo banners", error) } } - private fun onBannerShown(displayId: String) { + private fun onBannerShown(displayId: Int) { if (shownBannerIds.add(displayId)) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Shown(displayId, placeholder)) } } - private fun onCarouselScrolled(displayId: String) { + private fun onCarouselScrolled(displayId: Int) { if (!wasCarouselScrolled) { wasCarouselScrolled = true analyticsEventHandler.send(PromoBannerAnalyticsEvent.CarouselScrolled(displayId, placeholder)) } } - private fun onButtonClick(displayId: String, deeplink: String?) { + private fun onButtonClick(displayId: Int, deeplink: String?) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Clicked(displayId, placeholder)) - deeplink?.let { linkHandler.navigate(it) } + deeplink?.let { deeplinkLauncher.launch(it) } } - private fun onBannerDismiss(walletId: String, displayId: String) { + private fun getInitialState() = PromoBannersBlockUM( + userWalletId = "", + initialPage = 0, + banners = persistentListOf(), + onBannerShown = {}, + onCarouselScrolled = {}, + onPageChanged = {}, + ) + + private fun onBannerDismiss(walletId: String, displayId: Int) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Dismissed(displayId, placeholder)) uiState.update { state -> state.copy( @@ -111,11 +134,9 @@ internal class PromoBannersBlockModel @Inject constructor( runSuspendCatching { repository.dismissBanner(walletId, displayId) }.onFailure { error -> - Timber.w( + TangemLogger.w( + "Failed to dismiss promo banner $displayId for wallet $walletId", error, - "Failed to dismiss promo banner %s for wallet %s", - displayId, - walletId, ) } } diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt index e58b9d432c..0b12f8cf7c 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt @@ -1,10 +1,12 @@ package com.tangem.features.promobanners.impl.model import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal data class PromoBannersBlockUM( - val banners: ImmutableList = persistentListOf(), - val onBannerShown: (displayId: String) -> Unit = {}, - val onCarouselScrolled: (displayId: String) -> Unit = {}, + val userWalletId: String, + val initialPage: Int, + val banners: ImmutableList, + val onBannerShown: (displayId: Int) -> Unit, + val onCarouselScrolled: (displayId: Int) -> Unit, + val onPageChanged: (displayId: Int) -> Unit, ) \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt index ba193f57e4..9a13d9fab1 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt @@ -28,7 +28,7 @@ internal class DefaultPromoBannersRepository( override suspend fun getBanners( walletId: String, placeholder: Placeholder, - locale: String, + languageISOCode: String, ): List { val key = BannersCacheKey(walletId, placeholder) @@ -38,7 +38,7 @@ internal class DefaultPromoBannersRepository( tangemTechApi.getPromoBannerDisplays( walletId = walletId, placeholder = placeholder.toApiValue(), - locale = locale, + languageISOCode = languageISOCode, ).getOrThrow() .items .map(converter::convert) @@ -52,7 +52,7 @@ internal class DefaultPromoBannersRepository( return banners } - override suspend fun dismissBanner(walletId: String, displayId: String) { + override suspend fun dismissBanner(walletId: String, displayId: Int) { cache.update(default = emptyMap()) { current -> current.mapValues { (key, banners) -> if (key.walletId == walletId) { diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt index e4e7430d98..0fd96683b7 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt @@ -5,7 +5,11 @@ import com.tangem.features.promobanners.impl.model.PromoBannerDisplay internal interface PromoBannersRepository { - suspend fun getBanners(walletId: String, placeholder: Placeholder, locale: String): List + suspend fun getBanners( + walletId: String, + placeholder: Placeholder, + languageISOCode: String, + ): List - suspend fun dismissBanner(walletId: String, displayId: String) + suspend fun dismissBanner(walletId: String, displayId: Int) } \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt index 37b834d756..49cff50a0a 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,12 +36,16 @@ internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier = modifier = modifier, ) } else { - BannersCarousel( - banners = state.banners, - onBannerShown = state.onBannerShown, - onCarouselScroll = state.onCarouselScrolled, - modifier = modifier, - ) + key(state.userWalletId) { + BannersCarousel( + banners = state.banners, + initialPage = state.initialPage, + onBannerShown = state.onBannerShown, + onCarouselScroll = state.onCarouselScrolled, + onPageChange = state.onPageChanged, + modifier = modifier, + ) + } } } @@ -56,16 +61,22 @@ private fun SingleBanner(banner: PromoBannerNotificationUM, modifier: Modifier = @Composable private fun BannersCarousel( banners: List, - onBannerShown: (String) -> Unit, - onCarouselScroll: (String) -> Unit, + initialPage: Int, + onBannerShown: (Int) -> Unit, + onCarouselScroll: (Int) -> Unit, + onPageChange: (Int) -> Unit, modifier: Modifier = Modifier, ) { - val pagerState = rememberPagerState(pageCount = { banners.size }) + val pagerState = rememberPagerState( + initialPage = initialPage, + pageCount = { banners.size }, + ) LaunchedEffect(pagerState, banners) { snapshotFlow { pagerState.currentPage } .collect { page -> banners.getOrNull(page)?.let { banner -> + onPageChange(banner.displayId) onBannerShown(banner.displayId) if (page == 1) { // one-time event when user scrolls from first to second page, diff --git a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt index 91eb08c41a..36d87c263e 100644 --- a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt +++ b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt @@ -12,7 +12,7 @@ class PromoBannerDisplayDTOConverterTest { @Test fun `should convert DTO to domain model`() { val dto = PromoBannerDisplayDTO( - id = "123", + id = 123, placeholder = "MAIN", priority = "HIGH", title = "Test Banner", @@ -26,7 +26,7 @@ class PromoBannerDisplayDTOConverterTest { val result = converter.convert(dto) - assertThat(result.id).isEqualTo("123") + assertThat(result.id).isEqualTo(123) assertThat(result.placeholder).isEqualTo("MAIN") assertThat(result.priority).isEqualTo(PromoBannerPriority.HIGH) assertThat(result.title).isEqualTo("Test Banner") @@ -63,7 +63,7 @@ class PromoBannerDisplayDTOConverterTest { placeholder: String = "MAIN", priority: String = "MEDIUM", ) = PromoBannerDisplayDTO( - id = "1", + id = 1, placeholder = placeholder, priority = priority, title = "Title", diff --git a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt index fed421aa20..fdae92bc97 100644 --- a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt +++ b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt @@ -13,14 +13,14 @@ class PromoBannerDisplayToNotificationConverterTest { @Test fun `should convert banner with all fields`() { val banner = createBanner( - id = "b1", + id = 1, deeplink = "tangem://wallet", isButtonEnabled = true, buttonText = "Open", isDismissable = true, ) var clickedDeeplink: String? = "not_called" - var dismissedId: String? = null + var dismissedId: Int? = null val result = converter.convert( banner = banner, @@ -28,7 +28,7 @@ class PromoBannerDisplayToNotificationConverterTest { onDismiss = { dismissedId = it }, ) - assertThat(result.displayId).isEqualTo("b1") + assertThat(result.displayId).isEqualTo(1) assertThat(result.config.title).isEqualTo(TextReference.Str("Title")) assertThat(result.config.subtitle).isEqualTo(TextReference.Str("Subtitle")) assertThat(result.config.iconUrl).isEqualTo("https://icon.png") @@ -42,7 +42,7 @@ class PromoBannerDisplayToNotificationConverterTest { assertThat(clickedDeeplink).isEqualTo("tangem://wallet") result.config.onCloseClick!!.invoke() - assertThat(dismissedId).isEqualTo("b1") + assertThat(dismissedId).isEqualTo(1) } @Test @@ -98,7 +98,7 @@ class PromoBannerDisplayToNotificationConverterTest { } private fun createBanner( - id: String = "id", + id: Int = 0, deeplink: String? = "https://example.com", isButtonEnabled: Boolean = false, buttonText: String? = null, diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index e8a673832e..1765878318 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.compose.accompanist.permission) /** Other dependencies */ - implementation(deps.timber) implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) @@ -34,6 +33,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.utils) /** Common modules */ implementation(projects.common.routing) diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 5e1a8a4c8f..9c92b28a75 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -60,6 +60,5 @@ dependencies { /** Other dependencies */ implementation(deps.arrow.core) - implementation(deps.timber) implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt index 8ee7c7ea48..adf60baa52 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt @@ -26,7 +26,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.delay -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.IOException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -63,8 +63,8 @@ class DefaultQrScanningComponent @AssistedInject constructor( } } - val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { - val selectedImage = it ?: Uri.EMPTY + val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + val selectedImage = uri ?: Uri.EMPTY if (selectedImage != Uri.EMPTY) { val mimeType = context.contentResolver.getType(selectedImage) if (mimeType.isImageMimeType()) { @@ -72,7 +72,7 @@ class DefaultQrScanningComponent @AssistedInject constructor( val image = InputImage.fromFilePath(context, selectedImage) analyzer.analyze(image) } catch (e: IOException) { - Timber.e(e, "Unable to get image $selectedImage from gallery") + TangemLogger.e("Unable to get image $selectedImage from gallery", e) } } } diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 3604d3a6be..e9ab3a55d8 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -35,7 +35,6 @@ dependencies { /** Dependencies */ implementation(deps.arrow.core) implementation(deps.jodatime) - implementation(deps.timber) implementation(tangemDeps.card.core) /** DI */ diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 5e67063fe0..3dac39dddb 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -13,7 +13,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class ReferralInteractorImpl( private val repository: ReferralRepository, @@ -62,7 +62,7 @@ internal class ReferralInteractorImpl( ) .mapLeft { it.mapToDomainError() } .onLeft { error -> - Timber.e(error) + TangemLogger.e("Error", error) if (error is ReferralError.UserCancelledException) { throw error } diff --git a/features/referral/impl/build.gradle.kts b/features/referral/impl/build.gradle.kts index 1ced33e61d..8f23358f8f 100644 --- a/features/referral/impl/build.gradle.kts +++ b/features/referral/impl/build.gradle.kts @@ -58,7 +58,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.decompose.ext.compose) implementation(deps.compose.accompanist.systemUiController) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt index cd54552b79..ce206c8140 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt @@ -8,7 +8,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultReferralDeepLinkHandler @AssistedInject constructor( appRouter: AppRouter, @@ -19,7 +19,7 @@ internal class DefaultReferralDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> if (userWallet !is UserWallet.Cold || userWallet.cardTypesResolver.isTangemWallet()) { diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 66e97a0ebc..81c2490591 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.datasource) + implementation(projects.core.utils) api(projects.core.pagination) /** Tangem SDK */ @@ -85,7 +86,6 @@ dependencies { /** Other dependencies */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.androidx.paging.runtime) /** DI */ diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt index 71f276e7e4..dec24258e2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt @@ -18,7 +18,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("ComplexCondition") internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( @@ -40,13 +40,13 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( getSelectedWalletSyncUseCase() .fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> if (currencyId.isNullOrEmpty() || transactionId.isNullOrEmpty() || amount.isNullOrEmpty() || destinationAddress.isNullOrEmpty() ) { - Timber.e( + TangemLogger.e( """ Invalid parameters for SELL deeplink |- Params: $queryParams @@ -57,7 +57,7 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( scope.launch { val cryptoCurrency = getCryptoCurrency(userWallet.walletId, currencyId).getOrElse { - Timber.e("Error on getting cryptoCurrency: $currencyId") + TangemLogger.e("Error on getting cryptoCurrency: $currencyId") return@launch } // Convert using universal parser to account for regional separators diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index 836b3534d5..ac7220cf8e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class FeeSelectorLogic @AssistedInject constructor( @@ -277,7 +277,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( val selectedToken = getSelectedTokenStatus(fee.feeTokenId).bind() val availableTokens = getAvailableFeeTokens().fold( ifLeft = { error -> - Timber.e("Failed to get available fee tokens: $error") + TangemLogger.e("Failed to get available fee tokens: $error") if (selectedToken.currency !is CryptoCurrency.Coin) { raise(error) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 710868cdef..4c9a5154ca 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -77,7 +77,7 @@ import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @@ -374,7 +374,7 @@ internal class SendConfirmModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error) + TangemLogger.e("Error", error) _uiState.update(SendConfirmSendingStateTransformer(isSending = false)) alertFactory.getGenericErrorState( onFailedTxEmailClick = { @@ -470,7 +470,7 @@ internal class SendConfirmModel @Inject constructor( val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network) manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 40d4600859..de06ea2a08 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -67,7 +67,7 @@ import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -271,7 +271,7 @@ internal class SendModel @Inject constructor( } override fun onError(error: GetUserWalletError) { - Timber.w(error.toString()) + TangemLogger.w(error.toString()) showAlertError() } @@ -430,7 +430,7 @@ internal class SendModel @Inject constructor( .launchIn(modelScope) }, ifLeft = { error -> - Timber.w(error.toString()) + TangemLogger.w(error.toString()) showAlertError() return@launch }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 084f793b58..b0a07a0db9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -57,7 +57,7 @@ import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @@ -273,7 +273,7 @@ internal class NFTSendConfirmModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error) + TangemLogger.e("Error", error) _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) alertFactory.getGenericErrorState( onFailedTxEmailClick = { onFailedTxEmailClick(error.localizedMessage.orEmpty()) }, @@ -297,7 +297,7 @@ internal class NFTSendConfirmModel @Inject constructor( result.fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) alertFactory.getSendTransactionErrorState( error = error, popBack = appRouter::pop, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 73df61850a..6d844d1315 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -13,9 +13,13 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase @@ -65,6 +69,7 @@ internal class SendDestinationModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, private val analyticsEventHandler: AnalyticsEventHandler, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, ) : Model(), SendDestinationClickIntents { @@ -217,8 +222,6 @@ internal class SendDestinationModel @Inject constructor( flow = getWalletsUseCase().conflate(), flow2 = multiAccountStatusListSupplier().conflate(), ) { wallets, accountStatusLists -> - val cryptoCurrencyNetwork = cryptoCurrency.network - coroutineScope { accountStatusLists.mapNotNull { accountStatusList -> val wallet = wallets @@ -227,35 +230,55 @@ internal class SendDestinationModel @Inject constructor( ?: return@mapNotNull null async { - accountStatusList.flattenCurrencies() - .filter { it.currency.network.rawId == cryptoCurrencyNetwork.rawId } - .mapNotNull { cryptoCurrencyStatus -> - val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: return@mapNotNull null - - // Find the corresponding account from accountStatuses - val account = accountStatusList.accountStatuses - .filterCryptoPortfolio() - .firstOrNull { accountStatus -> - accountStatus.tokenList - .flattenCurrencies() - .any { it.currency.id == cryptoCurrencyStatus.currency.id } - }?.account - - DestinationWalletUM( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrencyStatus.currency, - userWalletId = wallet.walletId, - account = account, - ) + accountStatusList.accountStatuses.flatMap { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.getDestinationWalletUM(wallet) + is AccountStatus.Payment -> listOfNotNull(accountStatus.getDestinationWalletUM(wallet)) } + } } }.awaitAll().flatten() } }.flowOn(dispatchers.default) } + private fun AccountStatus.CryptoPortfolio.getDestinationWalletUM(wallet: UserWallet): List { + return this.flattenCurrencies() + .filter { it.currency.network.rawId == cryptoCurrency.network.rawId } + .mapNotNull { cryptoCurrencyStatus -> + val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: return@mapNotNull null + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = wallet.walletId, + account = account, + ) + } + } + + private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? { + val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null + val address = when (val status = this.value) { + is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress + is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress + else -> return null + } + val currency = tangemPayCryptoCurrencyFactory.create(wallet).getOrNull() ?: return null + return if (contractAddress.equals(currency.contractAddress, true)) { + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = currency, + userWalletId = wallet.walletId, + account = account, + ) + } else { + null + } + } + private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) { modelScope.launch { _uiState.update(SendDestinationValidationStartedTransformer) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt index 5b47c7de51..6ac08e8caf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -35,10 +35,11 @@ internal class SendRecipientWalletListConverter( return this.filterNotNull() .filter { destinationWallet -> val isCoin = destinationWallet.cryptoCurrency is CryptoCurrency.Coin + val isPaymentAccount = destinationWallet.account is Account.Payment val isNotSameAddress = destinationWallet.address != senderAddress val isNotBlankAddress = destinationWallet.address.isNotBlank() - isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable) + isNotBlankAddress && (isCoin || isPaymentAccount) && (isNotSameAddress || isSelfSendAvailable) } .groupBy { item -> item.name } .values.map { wallets -> diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 72ddfd69ee..f2d9d8b1cf 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.lifecycle.compose) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.moshi) /** Compose */ diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index 9c90294738..c861dfb887 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -17,7 +17,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( @@ -44,7 +44,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( // If selected user wallet is different than from deeplink - ignore deeplink // If selected user wallet is null - ignore deeplink if (walletId != selectedUserWalletId || selectedUserWalletId == null) { - Timber.e("Error on getting user wallet") + TangemLogger.e("Error on getting user wallet") return } @@ -60,7 +60,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( } if (cryptoCurrency == null) { - Timber.e( + TangemLogger.e( """ Could not get crypto currency for |- $NETWORK_ID_KEY: $networkId @@ -77,7 +77,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( val option = (availability as? StakingAvailability.Available)?.option if (option == null) { - Timber.e("Staking is unavailable for ${cryptoCurrency.name}") + TangemLogger.e("Staking is unavailable for ${cryptoCurrency.name}") return@launch } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 27bf32458e..a45bcd44b5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -106,7 +106,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -835,7 +835,7 @@ internal class StakingModel @Inject constructor( userWalletId = userWalletId, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) analyticsEventHandler.send( StakingAnalyticsEvent.TransactionError( errorCode = "CreateApprovalTxError", @@ -864,7 +864,7 @@ internal class StakingModel @Inject constructor( network = tokenCryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) analyticsEventHandler.send( StakingAnalyticsEvent.TransactionError( errorCode = error.getAnalyticsDescription(), @@ -1438,7 +1438,7 @@ internal class StakingModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, ).getOrElse { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) false } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt index c0e37364e6..ca9f4e5992 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt @@ -39,7 +39,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal @Suppress("LongParameterList") @@ -286,7 +286,7 @@ internal class StakeKitTransactionSender @AssistedInject constructor( transactionHash = transactionHash, ) }.onRight { - Timber.d("Successful hash submission") + TangemLogger.d("Successful hash submission") } } } diff --git a/features/stories/impl/build.gradle.kts b/features/stories/impl/build.gradle.kts index 077cadf096..bb49f8b167 100644 --- a/features/stories/impl/build.gradle.kts +++ b/features/stories/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.res) implementation(projects.core.ui) + implementation(projects.core.utils) implementation(projects.core.analytics) /** AndroidX */ @@ -37,10 +38,9 @@ dependencies { implementation(deps.compose.ui.tooling) /** Others */ - implementation(deps.kotlin.immutable.collections) - implementation(deps.compose.coil) - implementation(deps.timber) implementation(deps.arrow.core) + implementation(deps.compose.coil) + implementation(deps.kotlin.immutable.collections) /** DI */ implementation(deps.hilt.android) diff --git a/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt b/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt index 875871e2cd..35c985a28a 100644 --- a/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt +++ b/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class StoriesModel @Inject constructor( @@ -52,7 +52,7 @@ internal class StoriesModel @Inject constructor( modelScope.launch { getStoryContentUseCase.invokeSync(params.storyId).fold( ifLeft = { - Timber.e("Unable to load stories for ${params.storyId}") + TangemLogger.e("Unable to load stories for ${params.storyId}") openScreen(hideStories = false) }, ifRight = { story -> diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index b64e7ab8d8..612a39dcc8 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.datasource) implementation(projects.core.analytics) + implementation(projects.core.utils) /** Common */ implementation(projects.common.ui) @@ -85,7 +86,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.decompose) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.kotlin.immutable.collections) implementation(deps.coil) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt index 035c016356..91b726bd17 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt @@ -5,15 +5,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.navigationButtons.NavigationModelCallback import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountContent +import com.tangem.features.swap.v2.impl.sendviaswap.rateinfo.SwapRateInfoComponent import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -24,6 +32,14 @@ internal class SwapAmountComponent @AssistedInject constructor( private val model: SwapAmountModel = getOrCreateModel(params = params) + private val rateInfoSlot = childSlot( + source = model.rateInfoNavigation, + key = "rateInfoSlot", + serializer = null, + handleBackButton = true, + childFactory = ::rateInfoChild, + ) + init { lifecycle.subscribe( onStart = model::onStart, @@ -36,12 +52,26 @@ internal class SwapAmountComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val amountUM by model.uiState.collectAsStateWithLifecycle() + val rateInfo by rateInfoSlot.subscribeAsState() SwapAmountContent( amountUM = amountUM, modifier = Modifier.background(TangemTheme.colors.background.tertiary), clickIntents = model, ) + + rateInfo.child?.instance?.BottomSheet() + } + + private fun rateInfoChild( + config: ExpressRateType, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + return SwapRateInfoComponent( + appComponentContext = childByContext(componentContext), + expressRateType = config, + onDismiss = { model.rateInfoNavigation.dismiss() }, + ) } interface ModelCallback : NavigationModelCallback { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index c749636dc6..b5cdb3898e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.ImmutableList @@ -44,6 +45,7 @@ internal sealed class SwapAmountUM { // selected swap route val swapRateType: ExpressRateType, + val swapRateMode: SwapRateMode, // swap models val swapCurrencies: SwapCurrencies, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index 0e9ba829e8..38716f07d8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -8,6 +8,7 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onExpandEditField(selectedAmountType: SwapAmountType) fun onInfoClick() fun onSelectTokenClick() + fun onRateClick() fun onSeparatorClick() fun onProviderClick() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 2b3fc14c6d..98c238edb1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -16,17 +16,18 @@ 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.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError -import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase @@ -63,7 +64,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -108,6 +109,7 @@ internal class SwapAmountModel @Inject constructor( private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val rateInfoNavigation: SlotNavigation = SlotNavigation() private var isShowBestRateAnimation: Boolean = false @@ -172,17 +174,43 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + isBalanceHidden = params.isBalanceHidingFlow.value, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ), ) } override fun onExpandEditField(selectedAmountType: SwapAmountType) { + val content = uiState.value as? SwapAmountUM.Content ?: return + + val newSwapRateType = when (selectedAmountType) { + SwapAmountType.To -> { + if (content.swapRateMode != SwapRateMode.FLOAT_ONLY) { + ExpressRateType.Fixed + } else { + return + } + } + SwapAmountType.From -> { + if (content.swapRateMode == SwapRateMode.FIXED_ONLY) { + ExpressRateType.Fixed + } else if (content.swapRateMode == SwapRateMode.FLOAT_AND_FIXED) { + ExpressRateType.Float + } else { + return + } + } + } + uiState.update { amountUM -> - if (amountUM !is SwapAmountUM.Content) return + if (amountUM !is SwapAmountUM.Content) return@update amountUM amountUM.copy( selectedAmountType = selectedAmountType, + swapRateType = newSwapRateType, ) } + startLoadingQuotesTask(isSilentReload = false) } override fun onInfoClick() { @@ -290,6 +318,11 @@ internal class SwapAmountModel @Inject constructor( } } + override fun onRateClick() { + val content = uiState.value as? SwapAmountUM.Content ?: return + rateInfoNavigation.activate(content.swapRateType) + } + override fun onSeparatorClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return @@ -335,17 +368,9 @@ internal class SwapAmountModel @Inject constructor( private fun subscribeOnBalanceHiddenUpdates() { params.isBalanceHidingFlow.onEach { isHidden -> - val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 uiState.transformerUpdate( SwapAmountBalanceHiddenTransformer( isBalanceHidden = isHidden, - isSingleWallet = isOnlyOneWallet, - userWallet = userWallet, - appCurrency = appCurrency, - swapDirection = swapDirection, - clickIntents = this, - isAccountsMode = params.isAccountModeFlow.value, - account = params.accountFlow.value, ), ) }.launchIn(modelScope) @@ -525,16 +550,14 @@ internal class SwapAmountModel @Inject constructor( private fun initPairs(swapCurrencies: SwapCurrencies, secondaryCryptoCurrency: CryptoCurrency?) { modelScope.launch { - val swapCryptoCurrency = selectInitialPairUseCase( + val secondaryCurrency = selectInitialPairUseCase( primaryCryptoCurrency = primaryCryptoCurrency, secondaryCryptoCurrency = secondaryCryptoCurrency, userWallet = userWallet, swapCurrencies = swapCurrencies, swapDirection = params.swapDirection, ) - - val secondaryStatus = swapCryptoCurrency?.currencyStatus - + val secondaryStatus = secondaryCurrency?.currencyStatus val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (secondaryStatus != null && primaryStatus != null) { initCurrencies(primaryStatus, secondaryStatus) @@ -553,12 +576,18 @@ internal class SwapAmountModel @Inject constructor( isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, + providers = secondaryCurrency.providers, ), ) + + val currentState = uiState.value as? SwapAmountUM.Content + if (currentState != null && currentState.swapRateMode != SwapRateMode.FLOAT_ONLY) { + computeAndSetSecondaryAmount(currentState) + } startLoadingQuotesTask(isSilentReload = false) } else { @Suppress("NullableToStringCall") - Timber.e( + TangemLogger.e( """ Invalid cryptocurrencies status: | Primary -> $primaryStatus @@ -592,7 +621,11 @@ internal class SwapAmountModel @Inject constructor( fiatRate = secondaryStatus.value.fiatRate, fiatAmount = secondaryStatus.value.fiatAmount, ) - secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus) + secondaryMaximumAmountBoundary = EnterAmountBoundary( + amount = null, + fiatRate = secondaryStatus.value.fiatRate, + fiatAmount = null, + ) } } @@ -606,18 +639,39 @@ internal class SwapAmountModel @Inject constructor( onReverse = { state.secondaryCryptoCurrencyStatus.currency to state.primaryCryptoCurrencyStatus.currency }, ) - val fromAmount = when (state.swapDirection) { - SwapDirection.Direct -> state.primaryAmount.amountField - SwapDirection.Reverse -> state.secondaryAmount.amountField - } as? AmountState.Data + val amountField = state.swapDirection.withSwapDirection( + onDirect = { + if (state.selectedAmountType == SwapAmountType.From) { + state.primaryAmount.amountField + } else { + state.secondaryAmount.amountField + } + }, + onReverse = { + if (state.selectedAmountType == SwapAmountType.From) { + state.secondaryAmount.amountField + } else { + state.primaryAmount.amountField + } + }, + ) as? AmountState.Data - val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() + val amountValue = amountField?.amountTextField?.cryptoAmount?.value.orZero() val isAmountScreen = params is SwapAmountComponentParams.AmountParams - val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero() + val isAmountError = amountField?.amountTextField?.isError == true || amountValue.isNullOrZero() if (isAmountScreen && isAmountError) { uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return } + val rateType = when (state.selectedAmountType) { + SwapAmountType.To -> ExpressRateType.Fixed + SwapAmountType.From -> if (state.swapRateMode == SwapRateMode.FIXED_ONLY) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } + } + if (!isSilentReload) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) modelScope.launch { @@ -625,6 +679,7 @@ internal class SwapAmountModel @Inject constructor( .asSequence() .filter { swapCurrencyStatus -> swapCurrencyStatus.currencyStatus.currency.id == toCryptoCurrency.id } .flatMap(SwapCryptoCurrency::providers) + .filter { provider -> provider.rateTypes.contains(rateType) } .toList() .map { provider -> async { @@ -632,9 +687,9 @@ internal class SwapAmountModel @Inject constructor( userWallet = userWallet, fromCryptoCurrency = fromCryptoCurrency, toCryptoCurrency = toCryptoCurrency, - amount = fromAmountValue, - amountType = SwapAmountType.From, - rateType = ExpressRateType.Float, + amount = amountValue, + amountType = state.selectedAmountType, + rateType = rateType, provider = provider, ).fold( ifLeft = { error -> @@ -650,7 +705,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, allowanceContract = quote.allowanceContract, isApprovalNeeded = checkAllowance(state, quote), - fromAmount = fromAmountValue, + fromAmount = amountValue, ).convert( SwapQuoteUMConverter.Data( quote = quote, @@ -669,12 +724,48 @@ internal class SwapAmountModel @Inject constructor( secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, isNeedApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), + isBalanceHidden = params.isBalanceHidingFlow.value, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ), ) feeSelectorReloadTrigger.triggerUpdate() } } + /** + * Compute secondary amount from primary amount using fiat rate conversion + * secondaryAmount = primaryAmount × primaryFiatRate / secondaryFiatRate + * + */ + private fun computeAndSetSecondaryAmount(state: SwapAmountUM.Content) { + val secondaryStatus = state.secondaryCryptoCurrencyStatus ?: return + val primaryFiatRate = state.primaryCryptoCurrencyStatus.value.fiatRate ?: return + val secondaryFiatRate = secondaryStatus.value.fiatRate ?: return + if (secondaryFiatRate <= BigDecimal.ZERO) return + + val primaryAmountField = (state.primaryAmount as? SwapAmountFieldUM.Content) + ?.amountField as? AmountState.Data ?: return + val primaryAmount = primaryAmountField.amountTextField.cryptoAmount.value ?: return + if (primaryAmount <= BigDecimal.ZERO) return + + val secondaryAmount = primaryAmount + .multiply(primaryFiatRate) + .divide(secondaryFiatRate, secondaryStatus.currency.decimals, java.math.RoundingMode.HALF_UP) + + val secondaryValue = secondaryAmount.parseBigDecimal(secondaryStatus.currency.decimals) + + uiState.transformerUpdate( + SwapAmountValueChangeTransformer( + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + value = secondaryValue, + ), + ) + } + private fun startLoadingQuotesTask(isSilentReload: Boolean) { quoteTaskScheduler.cancelTask() loadQuotes(isSilentReload = isSilentReload) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index f3b1614ce2..f0fee9face 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -58,9 +58,9 @@ internal object SwapAmountQuoteUtils { ): SwapAmountUM { if (this !is SwapAmountUM.Content) return this - val updatedAmountField = if ( - selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct - ) { + val isPrimaryFieldEdited = selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct + + val updatedAmountField = if (isPrimaryFieldEdited) { val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus) } else { @@ -68,9 +68,9 @@ internal object SwapAmountQuoteUtils { val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus) } - return copy( - primaryAmount = updatedAmountField, + primaryAmount = if (isPrimaryFieldEdited) updatedAmountField else primaryAmount, + secondaryAmount = if (isPrimaryFieldEdited) secondaryAmount else updatedAmountField, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 9adad0ddeb..6bfb4de5bd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -5,12 +5,8 @@ import com.tangem.common.ui.amountScreen.converters.AmountAccountConverter import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.models.AmountParameters -import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -19,7 +15,7 @@ import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM -import com.tangem.utils.StringsSigns.DOT +import java.math.BigDecimal @Suppress("LongParameterList") internal class SwapAmountFieldConverter( @@ -36,24 +32,34 @@ internal class SwapAmountFieldConverter( private val iconStateConverter = CryptoCurrencyToIconStateConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter() - fun convert(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus): SwapAmountFieldUM { + fun convert( + swapAmountType: SwapAmountType, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isSelected: Boolean, + isAmountEmpty: Boolean = true, + displayAmount: BigDecimal? = null, + ): SwapAmountFieldUM { val walletTitle = if (isSingleWallet) { resourceReference(R.string.send_from_title) } else { resourceReference(R.string.send_from_wallet_name, wrappedList(userWallet.name)) } + val subtitles = computeSubtitles( + swapAmountType = swapAmountType, + cryptoCurrencyStatus = cryptoCurrencyStatus, + isEntering = isSelected, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) return SwapAmountFieldUM.Content( - amountType = selectedType, + amountType = swapAmountType, title = stringReference(cryptoCurrencyStatus.currency.name), - subtitleLeft = getSubtitleLeft(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), - subtitleEllipsisLeft = getSubtitleEllipsisLeft( - selectedType = selectedType, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ), - subtitleRight = getSubtitleRight(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), - subtitleEllipsisRight = TextEllipsis.OffsetEnd(appCurrency.symbol.length), + subtitleLeft = subtitles.subtitleLeft, + subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, + subtitleRight = subtitles.subtitleRight, + subtitleEllipsisRight = subtitles.subtitleEllipsisRight, priceImpact = null, - isClickEnabled = selectedType.isViewingField(), + isClickEnabled = true, amountField = AmountStateConverter( clickIntents = clickIntents, appCurrency = appCurrency, @@ -65,8 +71,8 @@ internal class SwapAmountFieldConverter( isAccountsMode = isAccountsMode, walletTitle = walletTitle, prefixText = when { - selectedType.isEnteringField() -> resourceReference(R.string.common_from) - selectedType.isViewingField() -> resourceReference(R.string.common_to) + swapAmountType.isEnteringField() -> resourceReference(R.string.common_from) + swapAmountType.isViewingField() -> resourceReference(R.string.common_to) else -> TextReference.Companion.EMPTY }, ).convert(account), @@ -79,44 +85,29 @@ internal class SwapAmountFieldConverter( ) } - private fun getSubtitleLeft(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> combinedReference( - stringReference( - cryptoCurrencyStatus.value.amount.format { - crypto(cryptoCurrency = cryptoCurrencyStatus.currency) - }, - ), - ).orMaskWithStars(isBalanceHidden) - selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_get_amount) - else -> TextReference.Companion.EMPTY + private fun computeSubtitles( + swapAmountType: SwapAmountType, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult = when (swapAmountType) { + SwapAmountType.From -> SwapFromSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + SwapAmountType.To -> SwapToSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) } - private fun getSubtitleRight(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> if (isBalanceHidden) { - TextReference.EMPTY - } else { - combinedReference( - stringReference(value = " $DOT "), - stringReference( - cryptoCurrencyStatus.value.fiatAmount.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - ), - ) - } - else -> TextReference.EMPTY - } - - private fun getSubtitleEllipsisLeft(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = - when { - selectedType.isEnteringField() -> TextEllipsis.OffsetEnd(cryptoCurrencyStatus.currency.symbol.length) - selectedType.isViewingField() -> TextEllipsis.End - else -> TextEllipsis.End - } - private fun SwapAmountType.isEnteringField(): Boolean { return this == SwapAmountType.From && swapDirection == SwapDirection.Direct || this == SwapAmountType.To && swapDirection == SwapDirection.Reverse diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt new file mode 100644 index 0000000000..1ed8a5c867 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import java.math.BigDecimal + +/** + * Updates all subtitle fields on an existing [SwapAmountFieldUM.Content] field. + * + * Delegates to [SwapFromSubtitleConverter] / [SwapToSubtitleConverter] based on the field's + * [SwapAmountFieldUM.Content.amountType]. All other field properties are preserved. + * + * `isEntering` is derived internally: the field is "entering" when its amountType matches + * [selectedAmountType], so callers cannot accidentally pass a wrong value. + * + * @param selectedAmountType the currently selected amount type from [SwapAmountUM.Content] + * @param isBalanceHidden whether balance values should be masked + */ +internal class SwapAmountUpdateSubtitleConverter( + private val selectedAmountType: SwapAmountType, + private val isBalanceHidden: Boolean, +) { + + /** + * Updates all subtitle fields after a quote arrives. + * + * @param field the current field content to update + * @param cryptoCurrencyStatus status used for balance formatting + * @param isAmountEmpty whether the amount in this field is empty/null + * @param displayAmount optional override for the displayed amount (e.g. quote toAmount/fromAmount) + */ + fun updateSubtitles( + field: SwapAmountFieldUM.Content, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isAmountEmpty: Boolean, + displayAmount: BigDecimal? = null, + ): SwapAmountFieldUM.Content { + val isEntering = field.amountType == selectedAmountType + val subtitles = when (field.amountType) { + SwapAmountType.From -> SwapFromSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + SwapAmountType.To -> SwapToSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + } + return field.copy( + subtitleLeft = subtitles.subtitleLeft, + subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, + subtitleRight = subtitles.subtitleRight, + subtitleEllipsisRight = subtitles.subtitleEllipsisRight, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt new file mode 100644 index 0000000000..ed5c112fd0 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt @@ -0,0 +1,69 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import java.math.BigDecimal + +/** + * Computes all subtitle fields for the **From** (primary) field. + * + * | State | subtitleLeft | subtitleRight | ellipsisLeft | + * |--------------------------------|-----------------------------------|-----------------------------------|------------------------| + * | Entering (float), any | "Balance: " (empty param) | "{balance}" masked | OffsetEnd(symbol) | + * | Viewing (fixed), empty | "Balance: " (empty param) | "{balance}" masked (crypto only) | End | + * | Viewing (fixed), not empty | send_from_title | "{displayStr}" masked | OffsetEnd(symbol) | + */ +internal object SwapFromSubtitleConverter { + + fun convert( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isBalanceHidden: Boolean, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult { + val symbol = cryptoCurrencyStatus.currency.symbol + val balance = cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } + val displayStr = displayAmount?.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } ?: balance + + val subtitleLeft: TextReference + val subtitleRight: TextReference + val ellipsisLeft: TextEllipsis + + when { + isEntering -> { + subtitleLeft = resourceReference(R.string.common_balance, wrappedList("")) + subtitleRight = combinedReference(stringReference(balance)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) + } + !isEntering && isAmountEmpty -> { + subtitleLeft = resourceReference(R.string.common_balance, wrappedList("")) + subtitleRight = combinedReference(stringReference(balance)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.End + } + else -> { + subtitleLeft = resourceReference(R.string.send_from_title) + subtitleRight = combinedReference(stringReference(displayStr)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) + } + } + + return SwapSubtitleResult( + subtitleLeft = subtitleLeft, + subtitleRight = subtitleRight, + subtitleEllipsisLeft = ellipsisLeft, + subtitleEllipsisRight = TextEllipsis.OffsetEnd(symbol.length), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index c014e539e7..c521bbceb9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -42,6 +42,10 @@ internal class SwapQuoteUMConverter( append(rate.format { crypto(secondaryCurrency) }) } + val fromAmountValue = stringReference( + quote.fromTokenAmount?.format { crypto(primaryCurrency) }.orEmpty(), + ) + return if (allowanceContract != null) { if (isApprovalNeeded) { SwapQuoteUM.Allowance( @@ -51,25 +55,31 @@ internal class SwapQuoteUMConverter( } else { SwapQuoteUM.Content( provider = provider, - quoteAmount = quote.toTokenAmount, + toAmount = quote.toTokenAmount, + fromAmount = quote.fromTokenAmount, diffPercent = DifferencePercent.Empty, - quoteAmountValue = stringReference( + toAmountValue = stringReference( quote.toTokenAmount.toQuoteValue(), ), + fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, + quoteId = quote.quoteId, ) } } else { SwapQuoteUM.Content( provider = provider, - quoteAmount = quote.toTokenAmount, + toAmount = quote.toTokenAmount, + fromAmount = quote.fromTokenAmount, diffPercent = DifferencePercent.Empty, - quoteAmountValue = stringReference( + toAmountValue = stringReference( quote.toTokenAmount.toQuoteValue(), ), + fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, + quoteId = quote.quoteId, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt new file mode 100644 index 0000000000..3253f56f44 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.TextReference + +internal data class SwapSubtitleResult( + val subtitleLeft: TextReference, + val subtitleRight: TextReference, + val subtitleEllipsisLeft: TextEllipsis, + val subtitleEllipsisRight: TextEllipsis, +) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt new file mode 100644 index 0000000000..cba9789884 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt @@ -0,0 +1,73 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import com.tangem.utils.StringsSigns.TILDE_SIGN +import java.math.BigDecimal + +/** + * Computes all subtitle fields for the **To** (secondary) field. + * + * | State | subtitleLeft | subtitleRight | ellipsisLeft | + * |--------------------------------|------------------------------------|----------------------------|------------------------| + * | Viewing (float), empty | "Will be sent to recipient" | EMPTY | End | + * | Viewing (float), not empty | "Recipient gets" (empty param) | "~{displayStr}" masked | End | + * | Entering (fixed), any | "Will be sent to recipient" | EMPTY | End | + */ +internal object SwapToSubtitleConverter { + + fun convert( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isBalanceHidden: Boolean, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult { + val symbol = cryptoCurrencyStatus.currency.symbol + val balance = cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } + val displayStr = displayAmount?.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } ?: balance + + val subtitleLeft: TextReference + val subtitleRight: TextReference + val ellipsisLeft: TextEllipsis + + when { + isEntering -> { + subtitleLeft = resourceReference(R.string.send_amount_receive_token_subtitle) + subtitleRight = TextReference.EMPTY + ellipsisLeft = TextEllipsis.End + } + !isEntering && isAmountEmpty -> { + subtitleLeft = resourceReference(R.string.send_amount_receive_token_subtitle) + subtitleRight = TextReference.EMPTY + ellipsisLeft = TextEllipsis.End + } + else -> { + subtitleLeft = resourceReference( + R.string.send_with_swap_recipient_get_amount, + wrappedList(""), + ) + subtitleRight = combinedReference( + stringReference(TILDE_SIGN), + stringReference(displayStr), + ).orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.End + } + } + + return SwapSubtitleResult( + subtitleLeft = subtitleLeft, + subtitleRight = subtitleRight, + subtitleEllipsisLeft = ellipsisLeft, + subtitleEllipsisRight = TextEllipsis.OffsetEnd(symbol.length), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt index ede7a349fb..1fe1635001 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt @@ -1,70 +1,55 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers -import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapAmountType -import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.transformer.Transformer -@Suppress("LongParameterList") internal class SwapAmountBalanceHiddenTransformer( private val isBalanceHidden: Boolean, - private val isSingleWallet: Boolean, - private val userWallet: UserWallet, - private val appCurrency: AppCurrency, - private val swapDirection: SwapDirection, - private val clickIntents: AmountScreenClickIntents, - private val isAccountsMode: Boolean, - private val account: Account?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { val content = prevState as? SwapAmountUM.Content ?: return prevState - val amountFieldConverter = SwapAmountFieldConverter( - swapDirection = swapDirection, + val quoteContent = content.selectedQuote as? SwapQuoteUM.Content + + val subtitleConverter = SwapAmountUpdateSubtitleConverter( + selectedAmountType = content.selectedAmountType, isBalanceHidden = isBalanceHidden, - userWallet = userWallet, - appCurrency = appCurrency, - clickIntents = clickIntents, - isSingleWallet = isSingleWallet, - isAccountsMode = isAccountsMode, - account = account, ) - val recalculatedPrimary = amountFieldConverter.convert( - selectedType = SwapAmountType.From, - cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus, - ) as SwapAmountFieldUM.Content - - val oldPrimary = content.primaryAmount as? SwapAmountFieldUM.Content - - val mergedAmountField = if ( - oldPrimary?.amountField is AmountState.Data && recalculatedPrimary.amountField is AmountState.Data - ) { - val oldData = oldPrimary.amountField - val newData = recalculatedPrimary.amountField - newData.copy( - amountTextField = oldData.amountTextField, - isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled, - isEditingDisabled = oldData.isEditingDisabled, - reduceAmountBy = oldData.reduceAmountBy, - isIgnoreReduce = oldData.isIgnoreReduce, + val updatedPrimary = (content.primaryAmount as? SwapAmountFieldUM.Content)?.let { primary -> + val isAmountEmpty = (primary.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value == null + subtitleConverter.updateSubtitles( + field = primary, + cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = quoteContent?.fromAmount, ) + } ?: content.primaryAmount + + val updatedSecondary = if (content.secondaryCryptoCurrencyStatus != null) { + (content.secondaryAmount as? SwapAmountFieldUM.Content)?.let { secondary -> + val isAmountEmpty = (secondary.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value == null + subtitleConverter.updateSubtitles( + field = secondary, + cryptoCurrencyStatus = content.secondaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = quoteContent?.toAmount, + ) + } ?: content.secondaryAmount } else { - recalculatedPrimary.amountField + content.secondaryAmount } - val updatedPrimaryAmount = recalculatedPrimary.copy( - amountField = mergedAmountField, + return content.copy( + primaryAmount = updatedPrimary, + secondaryAmount = updatedSecondary, ) - - return content.copy(primaryAmount = updatedPrimaryAmount) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt new file mode 100644 index 0000000000..122e946132 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt @@ -0,0 +1,76 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.utils.transformer.Transformer + +/** + * Sets a generic "Something went wrong" error on the selected amount field + * when all quotes returned errors that cannot be converted to a specific amount error. + * Returns [prevState] unchanged when the condition is not met. + */ +internal class SwapAmountErrorQuoteTransformer( + private val quotes: List, +) : Transformer { + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + if (!areAllQuotesUnrecoverableErrors(prevState)) return prevState + + val error = resourceReference(R.string.send_with_swap_something_went_wrong) + + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + + val newPrimaryAmount = if (isPrimarySelected) { + applyError(prevState.primaryAmount, error) + } else { + prevState.primaryAmount + } + + val newSecondaryAmount = if (!isPrimarySelected) { + applyError(prevState.secondaryAmount, error) + } else { + prevState.secondaryAmount + } + + return prevState.copy( + isPrimaryButtonEnabled = false, + primaryAmount = newPrimaryAmount, + secondaryAmount = newSecondaryAmount, + ) + } + + private fun areAllQuotesUnrecoverableErrors(state: SwapAmountUM.Content): Boolean { + if (!quotes.all { it is SwapQuoteUM.Error }) return false + + val secondaryProviderErrorConverter = state.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } + return quotes.all { quote -> + (quote as? SwapQuoteUM.Error)?.expressError + ?.let { secondaryProviderErrorConverter?.convert(it) } == null + } + } + + private fun applyError( + field: SwapAmountFieldUM, + error: com.tangem.core.ui.extensions.TextReference, + ): SwapAmountFieldUM { + val content = field as? SwapAmountFieldUM.Content ?: return field + val amountData = content.amountField as? AmountState.Data ?: return field + return content.copy( + amountField = amountData.copy( + amountTextField = amountData.amountTextField.copy( + error = error, + isError = true, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index bb5046a48d..e2d648a6c9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter @@ -46,8 +47,9 @@ internal class SwapAmountPrimaryReadyStateTransformer( isPrimaryButtonEnabled = false, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, primaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.From, + swapAmountType = SwapAmountType.From, cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + isSelected = prevState.selectedAmountType == SwapAmountType.From, ), secondaryCryptoCurrencyStatus = null, secondaryAmount = SwapAmountFieldUM.Empty( @@ -62,6 +64,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + swapRateMode = (prevState as? SwapAmountUM.Content)?.swapRateMode ?: SwapRateMode.FLOAT_ONLY, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 545c13db6e..cc49a10c46 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -2,13 +2,15 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM @@ -20,6 +22,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val userWallet: UserWallet, private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val providers: List, private val appCurrency: AppCurrency, private val swapCurrencies: SwapCurrencies, private val clickIntents: AmountScreenClickIntents, @@ -43,24 +46,41 @@ internal class SwapAmountSecondaryReadyStateTransformer( ) override fun transform(prevState: SwapAmountUM): SwapAmountUM { + val rateTypes = providers.flatMapTo(mutableSetOf()) { it.rateTypes } + val swapRateMode = when { + rateTypes.containsAll(listOf(ExpressRateType.Float, ExpressRateType.Fixed)) -> SwapRateMode.FLOAT_AND_FIXED + rateTypes.contains(ExpressRateType.Fixed) -> SwapRateMode.FIXED_ONLY + else -> SwapRateMode.FLOAT_ONLY + } + val selectedAmountType = if (swapRateMode != SwapRateMode.FLOAT_ONLY) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } return SwapAmountUM.Content( isPrimaryButtonEnabled = false, primaryAmount = prevState.primaryAmount, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, secondaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.To, + swapAmountType = SwapAmountType.To, cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + isSelected = prevState.selectedAmountType == SwapAmountType.To, ), secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, swapCurrencies = swapCurrencies, - selectedAmountType = prevState.selectedAmountType, + selectedAmountType = if (selectedAmountType == ExpressRateType.Fixed) { + SwapAmountType.To + } else { + SwapAmountType.From + }, swapDirection = swapDirection, - swapRateType = ExpressRateType.Float, + swapRateType = selectedAmountType, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + swapRateMode = swapRateMode, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 3f3f9ac158..627c8e5881 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -4,12 +4,15 @@ import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTrans import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter +import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.extensions.orZero @@ -20,63 +23,162 @@ internal class SwapAmountSelectQuoteTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isNeedApplyFCARestrictions: Boolean, + private val isBalanceHidden: Boolean, + private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, + private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { + + @Suppress("CyclomaticComplexMethod", "LongMethod") override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val providerErrorConverter = SwapAmountErrorConverter( + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To + + val primaryProviderErrorConverter = SwapAmountErrorConverter( cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, ) + val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } - return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, - selectedQuote = quoteUM, - isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, - primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { - val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content - val amountField = swapAmountField?.amountField as? AmountState.Data + val quoteContent = quoteUM as? SwapQuoteUM.Content + val fromAmount = quoteContent?.fromAmount + val toAmount = quoteContent?.toAmount - val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError?.let(providerErrorConverter::convert) + val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - swapAmountField?.copy( + val subtitleConverter = SwapAmountUpdateSubtitleConverter( + selectedAmountType = prevState.selectedAmountType, + isBalanceHidden = isBalanceHidden, + ) + + val newPrimaryAmount = when { + fromAmount != null && primaryMaximumAmountBoundary != null -> { + primarySwapAmountField?.let { fromField -> + subtitleConverter.updateSubtitles( + field = fromField, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + isAmountEmpty = false, + displayAmount = fromAmount, + ).copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = fromAmount.parseBigDecimal( + prevState.primaryCryptoCurrencyStatus.currency.decimals, + ), + ).transform(fromField.amountField), + ) + } ?: prevState.primaryAmount + } + isPrimarySelected -> { + val amountField = primarySwapAmountField?.amountField as? AmountState.Data + val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError + ?.let(primaryProviderErrorConverter::convert) + primarySwapAmountField?.copy( amountField = amountField?.copy( amountTextField = amountField.amountTextField.copy( error = amountError ?: TextReference.EMPTY, isError = amountError != null, ), - ) ?: swapAmountField.amountField, + ) ?: primarySwapAmountField.amountField, ) ?: prevState.primaryAmount - } else { - prevState.primaryAmount - }, - secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From && - prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null - ) { - val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data) - ?.amountTextField?.cryptoAmount?.value.orZero() - val toAmount = (quoteUM as? SwapQuoteUM.Content)?.quoteAmount - val priceImpact = calculatePriceImpact( - swapDirection = prevState.swapDirection, - fromTokenAmount = fromAmount, - toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - ) - - secondaryAmountField?.copy( - priceImpact = priceImpact, + } + !isPrimarySelected && primarySwapAmountField != null && primaryMaximumAmountBoundary != null -> { + subtitleConverter.updateSubtitles( + field = primarySwapAmountField, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + isAmountEmpty = true, + ).copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) - .orEmpty(), - ).transform(secondaryAmountField.amountField), - ) ?: prevState.secondaryAmount + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = "", + ).transform(primarySwapAmountField.amountField), + ) + } + else -> prevState.primaryAmount + } + + val newSecondaryAmount = if ( + prevState.secondaryCryptoCurrencyStatus != null && + secondaryMaximumAmountBoundary != null && + secondarySwapAmountField != null + ) { + val fromAmountForPriceImpact = fromAmount + ?: (prevState.primaryAmount.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value.orZero() + val priceImpact = calculatePriceImpact( + swapDirection = prevState.swapDirection, + fromTokenAmount = fromAmountForPriceImpact, + toTokenAmount = toAmount.orZero(), + primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + ) + val isAmountEmpty = toAmount == null + val secondaryAmountError = if (isSecondarySelected) { + (quoteUM as? SwapQuoteUM.Error)?.expressError + ?.let { secondaryProviderErrorConverter?.convert(it) } } else { - prevState.secondaryAmount - }, + null + } + val transformedSecondaryAmountField = if (isSecondarySelected && toAmount == null) { + secondarySwapAmountField.amountField + } else { + AmountFieldChangeTransformer( + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) + .orEmpty(), + ).transform(secondarySwapAmountField.amountField) + } + val insufficientFundsError = if (isSecondarySelected && fromAmount != null) { + val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount + if (primaryBalance != null && fromAmount > primaryBalance) { + resourceReference(R.string.common_insufficient_balance) + } else { + null + } + } else { + null + } + val effectiveSecondaryError = secondaryAmountError ?: insufficientFundsError + val secondaryAmountFieldWithError = if (isSecondarySelected) { + (transformedSecondaryAmountField as? AmountState.Data)?.let { data -> + data.copy( + amountTextField = data.amountTextField.copy( + error = effectiveSecondaryError ?: TextReference.EMPTY, + isError = effectiveSecondaryError != null, + ), + ) + } ?: transformedSecondaryAmountField + } else { + transformedSecondaryAmountField + } + subtitleConverter.updateSubtitles( + field = secondarySwapAmountField, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = toAmount, + ).copy( + priceImpact = priceImpact, + amountField = secondaryAmountFieldWithError, + ) + } else { + prevState.secondaryAmount + } + + return prevState.copy( + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + selectedQuote = quoteUM, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + primaryAmount = newPrimaryAmount, + secondaryAmount = newSecondaryAmount, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index cd19623928..9b84a536bf 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent @@ -23,97 +24,151 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, private val isNeedApplyFcaRestrictions: Boolean, + private val isBalanceHidden: Boolean, + private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, + private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val isSingleProvider = quotes.filter { swapQuoteUM -> - swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || - (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError - }.isSingleItem() + val selectedAmountType = prevState.selectedAmountType + val comparator = SwapQuotesComparator(selectedAmountType) + val isSingleProvider = isSingleProvider(quotes) + val sortedQuotes = quotes.sortedWith(comparator) + val bestQuote = findBestQuote(quotes, comparator) ?: SwapQuoteUM.Empty - val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) - val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty - val quotesWithDiff = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider) - val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { - quotesWithDiff.firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId } - ?: prevState.selectedQuote - } else { - (bestQuote as? SwapQuoteUM.Content)?.copy( - diffPercent = DifferencePercent.Best, - isSingleProvider = isSingleProvider, - ) ?: bestQuote - } + val quotesWithDiff = getQuotesWithDiff( + sortedQuotes = sortedQuotes, + bestQuote = bestQuote, + isSingleProvider = isSingleProvider, + selectedAmountType = selectedAmountType, + ) + val selectedQuote = resolveSelectedQuote( + prevState = prevState, + quotesWithDiff = quotesWithDiff, + bestQuote = bestQuote, + isSingleProvider = isSingleProvider, + ) - val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( + val updatedState = SwapAmountSelectQuoteTransformer( quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && selectedQuote.provider?.isRestrictedByFCA() == true, - ) - - val updatedState = selectQuoteTransformer.transform(prevState = prevState) + isBalanceHidden = isBalanceHidden, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + ).transform(prevState = prevState) if (updatedState !is SwapAmountUM.Content) return prevState - return updatedState.copy( - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), + val stateWithError = SwapAmountErrorQuoteTransformer(quotes).transform(updatedState) + if (stateWithError !is SwapAmountUM.Content) return prevState + + return stateWithError.copy( + isPrimaryButtonEnabled = stateWithError.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(), + swapQuotes = quotesWithDiff, ) } + private fun resolveSelectedQuote( + prevState: SwapAmountUM.Content, + quotesWithDiff: ImmutableList, + bestQuote: SwapQuoteUM, + isSingleProvider: Boolean, + ): SwapQuoteUM { + if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { + return quotesWithDiff + .firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId } + ?: prevState.selectedQuote + } + return (bestQuote as? SwapQuoteUM.Content)?.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) ?: bestQuote + } + + private fun isSingleProvider(quotes: List): Boolean { + return quotes.filter { swapQuoteUM -> + swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || + (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + }.isSingleItem() + } + private fun getQuotesWithDiff( sortedQuotes: List, bestQuote: SwapQuoteUM, isSingleProvider: Boolean, + selectedAmountType: SwapAmountType, ): ImmutableList { - return sortedQuotes.sortedWith(SwapQuotesComparator) - .map { quote -> - if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { - if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy( - diffPercent = DifferencePercent.Best, - isSingleProvider = isSingleProvider, - ) - } else { - // current / selected - 1 - val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE - quote.copy( - diffPercent = DifferencePercent.Diff( - isPositive = percent.isPositive(), - percent = stringReference( - if (percent.isPositive()) { - "${StringsSigns.PLUS}${percent.format { percent() }}" - } else { - "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" - }, - ), - ), - ) - } + return sortedQuotes.map { quote -> + if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { + if (quote.provider.providerId == bestQuote.provider.providerId) { + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { - quote + val percent = if (selectedAmountType == SwapAmountType.To) { + // Fixed mode: best has lowest fromTokenAmount; compare as (best/current - 1) + val bestFrom = bestQuote.fromAmount + val quoteFrom = quote.fromAmount + if (bestFrom != null && quoteFrom != null && quoteFrom > BigDecimal.ZERO) { + bestFrom / quoteFrom - BigDecimal.ONE + } else { + BigDecimal.ZERO + } + } else { + // Float mode: best has highest toAmount; compare as (current/best - 1) + quote.toAmount / bestQuote.toAmount - BigDecimal.ONE + } + quote.copy( + diffPercent = DifferencePercent.Diff( + isPositive = percent.isPositive(), + percent = stringReference( + if (percent.isPositive()) { + "${StringsSigns.PLUS}${percent.format { percent() }}" + } else { + "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" + }, + ), + ), + ) } - }.toPersistentList() + } else { + quote + } + }.toPersistentList() } - private fun findBestQuote(quotes: List): SwapQuoteUM? { - return quotes - .sortedWith(SwapQuotesComparator) - .firstOrNull() + private fun findBestQuote(quotes: List, comparator: Comparator): SwapQuoteUM? { + return quotes.sortedWith(comparator).firstOrNull() } - private object SwapQuotesComparator : Comparator { + private class SwapQuotesComparator(private val selectedAmountType: SwapAmountType) : Comparator { override fun compare(p0: SwapQuoteUM?, p1: SwapQuoteUM?): Int { return when { p0 is SwapQuoteUM.Content && p1 !is SwapQuoteUM.Content -> -1 p0 !is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> 1 p0 is SwapQuoteUM.Error && p1 is SwapQuoteUM.Error -> compareErrorQuote(p0 = p0, p1 = p1) - p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> p1.quoteAmount.compareTo(p0.quoteAmount) + p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> compareContent(p0, p1) else -> 0 } } + private fun compareContent(p0: SwapQuoteUM.Content, p1: SwapQuoteUM.Content): Int { + return if (selectedAmountType == SwapAmountType.To) { + // Fixed mode: lower fromTokenAmount = better (ascending) + val f0 = p0.fromAmount ?: BigDecimal.ZERO + val f1 = p1.fromAmount ?: BigDecimal.ZERO + f0.compareTo(f1) + } else { + // Float mode: higher toAmount = better (descending) + p1.toAmount.compareTo(p0.toAmount) + } + } + private fun compareErrorQuote(p0: SwapQuoteUM.Error, p1: SwapQuoteUM.Error): Int = when { p0.expressError is ExpressError.AmountError && p1.expressError !is ExpressError.AmountError -> -1 p0.expressError !is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> 1 diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index b4df59fefc..d75eb733a2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -37,9 +37,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index b314e610b9..69d9cb7e19 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -39,9 +40,9 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.express.models.ExpressRateType -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountClickIntentsStub @@ -156,10 +157,12 @@ private fun SwapAmountBlock( Box { SwapAmountEditBlock( amountFieldUM = amountFieldUM, + isFixedRate = isFixedRate, modifier = Modifier, onValueChange = clickIntents::onAmountValueChange, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, onCurrencyChange = clickIntents::onCurrencyChangeClick, + onRateClick = clickIntents::onRateClick, ) HorizontalDivider( modifier = Modifier @@ -182,12 +185,15 @@ private fun SwapAmountBlock( } } +@Suppress("LongParameterList") @Composable private fun SwapAmountEditBlock( amountFieldUM: SwapAmountFieldUM, + isFixedRate: Boolean, onValueChange: (String) -> Unit, onValuePastedTriggerDismiss: () -> Unit, onCurrencyChange: (Boolean) -> Unit, + onRateClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -209,8 +215,44 @@ private fun SwapAmountEditBlock( onValueChange = onValueChange, onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, onCurrencyChange = onCurrencyChange, + reserveSpaceForError = false, modifier = Modifier, ) + SwapRateBadge( + isFixedRate = isFixedRate, + onClick = onRateClick, + ) + } +} + +@Composable +private fun SwapRateBadge(isFixedRate: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val iconRes = if (isFixedRate) R.drawable.ic_fixed else R.drawable.ic_floating + val textRes = if (isFixedRate) R.string.send_rate_is_fixed else R.string.send_rate_floating_info_title + Row( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .clickable(onClick = onClick) + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding(vertical = 2.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = iconRes)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + Text( + text = stringResourceSafe(textRes), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -227,6 +269,7 @@ private fun SwapAmountInfo( modifier: Modifier = Modifier, ) { val tokenIconState = (amountFieldUM.amountField as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading + val isEnabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -234,11 +277,11 @@ private fun SwapAmountInfo( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), - enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, + enabled = isEnabled, onClick = { - if (isFixedRate) { + if (!isSelectedAmountType) { onExpandEditField() - } else { + } else if (amountFieldUM.amountType == SwapAmountType.To) { onSelectTokenClick() } }, @@ -256,15 +299,14 @@ private fun SwapAmountInfo( SwapAmountInfoMain( amountFieldUM = amountFieldUM, selectedQuote = selectedQuote, - isSelectedAmountType = isSelectedAmountType, modifier = Modifier.weight(1f), ) AnimatedContent( targetState = isSelectedAmountType, ) { isSelected -> - if (isSelected) { + if (isSelected && amountFieldUM.amountType == SwapAmountType.From) { AmountMaxButton(onMaxAmountClick) - } else { + } else if (isSelected && amountFieldUM.amountType == SwapAmountType.To) { SwapAmountInfoQuote( quoteUM = selectedQuote, isFixedRate = isFixedRate, @@ -279,7 +321,6 @@ private fun SwapAmountInfo( private fun SwapAmountInfoMain( amountFieldUM: SwapAmountFieldUM, selectedQuote: SwapQuoteUM?, - isSelectedAmountType: Boolean, modifier: Modifier = Modifier, ) { Column( @@ -300,48 +341,52 @@ private fun SwapAmountInfoMain( ) } } - AnimatedContent(isSelectedAmountType) { isSelected -> - if (isSelected && amountFieldUM is SwapAmountFieldUM.Content) { - SpacerH2() - Row { - EllipsisText( - text = amountFieldUM.subtitleLeft.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = amountFieldUM.subtitleEllipsisLeft, - modifier = Modifier.weight(1f, fill = false), - ) - EllipsisText( - text = amountFieldUM.subtitleRight.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = amountFieldUM.subtitleEllipsisRight, - ) - } - } else { - AnimatedContent(selectedQuote) { quote -> - when (quote) { - is SwapQuoteUM.Content -> { - SpacerH2() + + AnimatedContent(selectedQuote) { quote -> + when (quote) { + is SwapQuoteUM.Content -> { + SpacerH2() + if (amountFieldUM is SwapAmountFieldUM.Content) { + SpacerH2() + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { EllipsisText( - text = stringResourceSafe( - R.string.send_with_swap_recipient_get_amount, - quote.quoteAmountValue.resolveReference(), - ), + text = amountFieldUM.subtitleLeft.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsisLeft, + modifier = Modifier.weight(1f, fill = false), ) - } - SwapQuoteUM.Loading -> { - SpacerH2() - TextShimmer( + EllipsisText( + text = amountFieldUM.subtitleRight.resolveReference(), style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), + color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsisRight, ) } - else -> Unit + } else { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) } } + SwapQuoteUM.Loading -> { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + else -> { + SpacerH2() + Text( + text = "", + style = TangemTheme.typography.caption2, + ) + } } } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 4a7380480b..351e841fba 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -10,6 +10,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onSelectTokenClick() {} + override fun onRateClick() {} + override fun onSeparatorClick() {} override fun onProviderClick() {} diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index b4ebcff7cb..6f354c847a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -12,10 +12,11 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.StringsSigns @@ -66,8 +67,10 @@ internal data object SwapAmountContentPreview { private val quote = SwapQuoteUM.Content( provider = provider, - quoteAmount = "123".toBigDecimal(), - quoteAmountValue = stringReference("123"), + toAmount = "123".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("123"), + fromAmountValue = TextReference.EMPTY, rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, isSingleProvider = false, @@ -92,6 +95,7 @@ internal data object SwapAmountContentPreview { appCurrency = AppCurrency.Default, isShowBestRateAnimation = false, isShowFCAWarning = false, + swapRateMode = SwapRateMode.FLOAT_ONLY, ) val defaultState = SwapAmountUM.Content( @@ -127,10 +131,11 @@ internal data object SwapAmountContentPreview { selectedQuote = quote, primaryCryptoCurrencyStatus = cryptoCurrencyStatus, secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, - swapRateType = ExpressRateType.Float, + swapRateType = ExpressRateType.Fixed, isPrimaryButtonEnabled = true, isShowBestRateAnimation = false, isShowFCAWarning = true, + swapRateMode = SwapRateMode.FLOAT_AND_FIXED, ) val defaultStateAccount: SwapAmountUM.Content diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 56c7a0f93a..7383f568e4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -41,7 +41,7 @@ internal class SwapProviderListItemConverter( title = stringReference(provider.name), subtitle = stringReference(provider.type.typeName), infoText = when (value) { - is SwapQuoteUM.Content -> value.quoteAmountValue + is SwapQuoteUM.Content -> value.toAmountValue else -> TextReference.EMPTY }, iconUrl = provider.imageLarge, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt index 54f3030f4f..a591206770 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -50,7 +50,7 @@ internal class SwapProviderStateConverter( name = provider.name, iconUrl = provider.imageLarge, type = provider.type.typeName, - subtitle = quoteAmountValue, + subtitle = toAmountValue, additionalBadge = additionalBadge, diffPercent = diffPercent, isSelected = provider == selectedProvider, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 238cf49cb3..4765401f15 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -35,8 +35,10 @@ internal object SwapChooseProviderContentPreview { private val quote1 = SwapQuoteUM.Content( provider = provider1, - quoteAmount = "123".toBigDecimal(), - quoteAmountValue = stringReference("123"), + toAmount = "123".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("123"), + fromAmountValue = stringReference(""), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, isSingleProvider = false, @@ -44,8 +46,10 @@ internal object SwapChooseProviderContentPreview { private val quote2 = SwapQuoteUM.Content( provider = provider2, - quoteAmount = "13.12".toBigDecimal(), - quoteAmountValue = stringReference("13.12"), + toAmount = "13.12".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("13.12"), + fromAmountValue = stringReference(""), rate = stringReference("1 USD ≈ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, isSingleProvider = false, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index a9312fc84a..b40d57b863 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -67,7 +67,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( private fun initContent() { val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { error -> - Timber.e("Failed to get user wallet: $error") + TangemLogger.e("Failed to get user wallet: $error") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return } @@ -77,7 +77,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( token = params.token, userWalletId = params.userWalletId, ).getOrElse { - Timber.e("Failed to get crypto currency") + TangemLogger.e("Failed to get crypto currency") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return@launch } @@ -88,7 +88,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES, swapTxType = SwapTxType.SendWithSwap, ).getOrElse { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index b4466b11a5..9a6c3072c5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -6,11 +6,9 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -25,7 +23,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet @@ -141,21 +138,6 @@ private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList.addInsufficientFundsNotification() { + val enteredFromAmount = notificationData.enteredFromAmount ?: return + val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return + if (enteredFromAmount > balance) { + add(SwapNotificationUM.Error.InsufficientFunds) + } + } + fun MutableList.addExpressErrorNotification() { val expressError = notificationData.expressError ?: return val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 59d34c0701..98b57000b7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -117,7 +117,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( is CryptoCurrency.Coin -> "0" }, memo = null, - amountValue = model.confirmData.enteredAmount.orZero(), + amountValue = model.confirmData.enteredFromAmount.orZero(), reduceAmountBy = model.confirmData.reduceAmountBy.orZero(), isIgnoreReduce = model.confirmData.isIgnoreReduce, fee = model.confirmData.fee, @@ -137,6 +137,8 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( memo = model.confirmData.enteredMemo, toCryptoCurrencyStatus = model.confirmData.toCryptoCurrencyStatus, userWalletId = params.userWallet.walletId, + enteredFromAmount = model.confirmData.enteredFromAmount, + fromCryptoCurrencyStatus = model.confirmData.fromCryptoCurrencyStatus, ), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index bea5544651..3c685ed2c6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -138,12 +138,18 @@ internal class SendWithSwapConfirmModel @Inject constructor( onDirect = { amountUM.primaryAmount }, onReverse = { amountUM.secondaryAmount }, ) - val amountState = fromAmount?.amountField as? AmountState.Data + val toAmount = amountUM?.swapDirection?.withSwapDirection( + onDirect = { amountUM.secondaryAmount }, + onReverse = { amountUM.primaryAmount }, + ) + val amountStateFrom = fromAmount?.amountField as? AmountState.Data + val amountStateTo = toAmount?.amountField as? AmountState.Data val isQuoteContent = amountUM?.selectedQuote is SwapQuoteUM.Content return ConfirmData( - enteredAmount = amountState?.amountTextField?.cryptoAmount?.value, - reduceAmountBy = amountState?.reduceAmountBy.takeIf { isQuoteContent }.orZero(), - isIgnoreReduce = amountState?.isIgnoreReduce == true, + enteredToAmount = amountStateTo?.amountTextField?.cryptoAmount?.value, + enteredFromAmount = amountStateFrom?.amountTextField?.cryptoAmount?.value, + reduceAmountBy = amountStateFrom?.reduceAmountBy.takeIf { isQuoteContent }.orZero(), + isIgnoreReduce = amountStateFrom?.isIgnoreReduce == true, enteredDestination = destinationUM?.addressTextField?.actualAddress, enteredMemo = destinationUM?.memoTextField?.value, fee = feeSelectorUM?.selectedFeeItem?.fee.takeIf { isQuoteContent }, @@ -250,7 +256,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( val defaultError = GetFeeError.UnknownError.left() val provider = (confirmData.quote as? SwapQuoteUM.Content)?.provider ?: return defaultError - val amountValue = confirmData.enteredAmount ?: return defaultError + val amountValue = confirmData.enteredFromAmount ?: return defaultError return when (val providerType = provider.type) { ExpressProviderType.CEX -> { @@ -274,7 +280,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either { val defaultError = GetFeeError.UnknownError.left() val provider = (confirmData.quote as? SwapQuoteUM.Content)?.provider ?: return defaultError - val amountValue = confirmData.enteredAmount ?: return defaultError + val amountValue = confirmData.enteredFromAmount ?: return defaultError return when (val providerType = provider.type) { ExpressProviderType.CEX -> { @@ -420,7 +426,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( is CryptoCurrency.Coin -> "0" }, memo = null, - amountValue = confirmData.enteredAmount.orZero(), + amountValue = confirmData.enteredFromAmount.orZero(), reduceAmountBy = confirmData.reduceAmountBy, isIgnoreReduce = confirmData.isIgnoreReduce, fee = confirmData.fee, @@ -436,6 +442,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( memo = confirmData.enteredMemo, toCryptoCurrencyStatus = confirmData.toCryptoCurrencyStatus, userWalletId = params.userWallet.walletId, + enteredFromAmount = confirmData.enteredFromAmount, + fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus, ), ) uiState.transformerUpdate( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 41f709edb1..080ab91574 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -25,10 +25,11 @@ import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransac import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils import com.tangem.features.swap.v2.impl.common.ConfirmData +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.RoundingMode @@ -67,7 +68,7 @@ internal class SwapTransactionSender @AssistedInject constructor( ExpressProviderType.DEX_BRIDGE, ExpressProviderType.ONRAMP, -> { - Timber.w("Provider $providerType is not supported in Send With Swap") + TangemLogger.w("Provider $providerType is not supported in Send With Swap") onExpressError(ExpressError.UnknownError) } } @@ -87,33 +88,43 @@ internal class SwapTransactionSender @AssistedInject constructor( val fromAccount = confirmData.fromAccount val provider = confirmData.quote?.provider ?: return val rateType = confirmData.rateType ?: return - val amountValue = confirmData.enteredAmount ?: return val feeValue = confirmData.fee?.amount?.value ?: return val destination = confirmData.enteredDestination ?: return - val fromAmount = FeeCalculationUtils.checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable = isAmountSubtractAvailable, - cryptoCurrencyStatus = fromStatus, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = confirmData.reduceAmountBy, - ) + val (amount, currencyStatus) = when (confirmData.amountType) { + SwapAmountType.From -> { + val amountValue = confirmData.enteredFromAmount ?: return + val subtracted = FeeCalculationUtils.checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = fromStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = confirmData.reduceAmountBy, + ) + subtracted to fromStatus + } + SwapAmountType.To -> { + val amountValue = confirmData.enteredToAmount ?: return + amountValue to toStatus + } + } val swapData = getSwapDataUseCase( userWallet = userWallet, fromCryptoCurrencyStatus = fromStatus, - amount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), - amountType = SwapAmountType.From, + amount = amount.toStringWithRightOffset(currencyStatus.currency.decimals), + amountType = confirmData.amountType, toCryptoCurrency = toStatus.currency, toAddress = destination, toExtraId = confirmData.enteredMemo, expressProvider = provider, rateType = rateType, expressOperationType = expressOperationType, + quoteId = (confirmData.quote as? SwapQuoteUM.Content)?.quoteId, ).getOrElse { error -> onExpressError(error); return } createAndSendCexTransaction( - fromAmount = fromAmount, + fromAmount = amount, fromStatus = fromStatus, fromAccount = fromAccount, toStatus = toStatus, @@ -155,7 +166,7 @@ internal class SwapTransactionSender @AssistedInject constructor( userWalletId = userWallet.walletId, network = fromStatus.currency.network, ).getOrElse { error -> - Timber.e(error, "Failed to create swap CEX tx data") + TangemLogger.e("Failed to create swap CEX tx data", error) onSendError(SendTransactionError.UnknownError(Exception(error))) return } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 039d065657..2ebed7f554 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -34,7 +34,7 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -159,7 +159,7 @@ internal class SendWithSwapModel @Inject constructor( getPrimaryCurrencyStatusUpdates(params.currency) }, ifLeft = { error -> - Timber.w(error.toString()) + TangemLogger.w(error.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt new file mode 100644 index 0000000000..29b381307f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt @@ -0,0 +1,33 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.rateinfo + +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.features.swap.v2.impl.R + +internal object SendWithSwapRateInfoFactory { + + fun getRateTypeMessage(expressRateType: ExpressRateType, onDismiss: () -> Unit): MessageBottomSheetUM { + val isFixed = expressRateType == ExpressRateType.Fixed + return messageBottomSheetUM { + onDismiss(onDismiss) + infoBlock { + iconImage(if (isFixed) R.drawable.ic_fixed_32 else R.drawable.ic_floating_32) + title = resourceReference( + if (isFixed) R.string.send_rate_fixed_info_title else R.string.send_rate_floating_info_title, + ) + body = resourceReference( + if (isFixed) { + R.string.send_rate_fixed_info_description + } else { + R.string.send_rate_floating_info_description + }, + ) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt new file mode 100644 index 0000000000..f7f7cb6c6f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt @@ -0,0 +1,27 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.rateinfo + +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.express.models.ExpressRateType + +internal class SwapRateInfoComponent( + appComponentContext: AppComponentContext, + expressRateType: ExpressRateType, + private val onDismiss: () -> Unit, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + private val state: MessageBottomSheetUM = SendWithSwapRateInfoFactory.getRateTypeMessage( + expressRateType = expressRateType, + onDismiss = onDismiss, + ) + + override fun dismiss() = onDismiss() + + @Composable + override fun BottomSheet() { + MessageBottomSheet(state = state, onDismissRequest = ::dismiss) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index 9e3064dafc..dab9d55aa2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.LinkAnnotation @@ -17,6 +18,7 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.common.ui.footers.SendingText +import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.Fade @@ -75,38 +77,62 @@ internal fun SendWithSwapContent( } } if (stackState.active.configuration != SendWithSwapRoute.Success) { - Column { - AnimatedVisibility( - visible = stackState.active.configuration == SendWithSwapRoute.Confirm, - enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), - exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), - ) { - val confirmContentUM = confirmUM as? ConfirmUM.Content - val sendFooter = confirmContentUM?.sendingFooter ?: TextReference.EMPTY - val legalFooter = getAnnotatedStringForLegals(confirmContentUM?.tosUM, onClick = onLinkClick) - SendingText( - footerText = if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) { - combinedReference(sendFooter, legalFooter) - } else { - TextReference.EMPTY - }, - ) - } - NavigationPrimaryButton( - navigationUMContent.primaryButton, - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - ) - } + SendWithSwapFooter( + confirmUM = confirmUM, + stackState = stackState, + primaryButton = navigationUMContent.primaryButton, + onLinkClick = onLinkClick, + ) } } } @Composable -private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick: (String) -> Unit): TextReference { +private fun SendWithSwapFooter( + confirmUM: ConfirmUM, + stackState: ChildStack, + primaryButton: NavigationButton, + onLinkClick: (String) -> Unit, +) { + Column { + AnimatedVisibility( + visible = stackState.active.configuration == SendWithSwapRoute.Confirm, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + val confirmContentUM = confirmUM as? ConfirmUM.Content + val sendFooter = confirmContentUM?.sendingFooter ?: TextReference.EMPTY + val legalFooter = getAnnotatedStringForLegals( + tosUM = confirmContentUM?.tosUM, + sendFooter = sendFooter, + onClick = onLinkClick, + ) + val footerText = remember(sendFooter, legalFooter) { + if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) { + combinedReference(sendFooter, legalFooter) + } else { + TextReference.EMPTY + } + } + SendingText(footerText = footerText) + } + NavigationPrimaryButton( + primaryButton = primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } +} + +@Composable +private fun getAnnotatedStringForLegals( + tosUM: ConfirmUM.Content.TosUM?, + sendFooter: TextReference, + onClick: (String) -> Unit, +): TextReference { if (tosUM == null) return TextReference.EMPTY val tos = tosUM.tosLink val policy = tosUM.policyLink @@ -118,7 +144,9 @@ private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick val policyIndex = fullString.indexOf(policyTitle) annotatedReference { - append(StringsSigns.POINT_SIGN) + if (!sendFooter.resolveReference().endsWith(StringsSigns.POINT_SIGN)) { + append(StringsSigns.POINT_SIGN) + } appendSpace() append(fullString.substring(0, tosIndex)) withLink( diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 4efc95da23..dd4eda6edb 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation(tangemDeps.blockchain) /** Others */ - implementation(deps.timber) implementation(deps.jodatime) /** DI */ diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 7e3ef9caeb..968145424e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -39,7 +39,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.IOException import java.math.BigDecimal import java.util.UUID @@ -253,7 +253,7 @@ internal class DefaultSwapRepository( ) }, catch = { exception -> - Timber.e("getExchangeStatus error: $exception") + TangemLogger.e("getExchangeStatus error: $exception") raise(UnknownError(exception.message)) }, ) @@ -344,6 +344,7 @@ internal class DefaultSwapRepository( appPreferencesStore = appPreferencesStore, ), toExtraId = toExtraId?.ifEmpty { null }, + quoteId = null, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) @@ -404,7 +405,7 @@ internal class DefaultSwapRepository( return try { txDetailsMoshiAdapter.fromJson(txDetailsJson) } catch (e: IOException) { - Timber.e(e, "error parsing txDetailsJson") + TangemLogger.e("error parsing txDetailsJson", e) null } } diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 02e0992d2b..d0baa14afb 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { /** Other Libraries **/ implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) - implementation(deps.timber) implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) implementation(deps.moshi) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3b7b8442ab..14b10c2aad 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -31,6 +31,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -70,7 +71,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -131,7 +132,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(userWalletId), - )?.accountStatuses.orEmpty() + )?.accountStatuses.orEmpty().filterCryptoPortfolio() val walletAccountCurrencyStatusesExceptInitial: Map> = walletAccountCurrencyStatuses.mapNotNull { accountStatus -> @@ -290,7 +291,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( contractAddress = permissionOptions.forTokenContractAddress, spenderAddress = permissionOptions.spenderAddress, ).getOrElse { error -> - Timber.e(error, "Failed to create approveTransaction") + TangemLogger.e("Failed to create approveTransaction", error) return SwapTransactionState.Error.UnknownError } @@ -321,7 +322,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( reduceBalanceBy: BigDecimal, txFeeSealedState: TxFeeSealedState, ): Map { - Timber.i( + TangemLogger.i( """ Find the best quote |- fromToken: $fromToken @@ -702,7 +703,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( expressOperationType: ExpressOperationType, isTangemPayWithdrawal: Boolean, ): SwapTransactionState { - Timber.i( + TangemLogger.i( """ Swap |- swapProvider: $swapProvider @@ -795,7 +796,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, txFee.fee.getGasLimit()), ).getOrElse { error -> - Timber.e(error, "Failed to create swap dex tx data") + TangemLogger.e("Failed to create swap dex tx data", error) return SwapTransactionState.Error.UnknownError } @@ -999,7 +1000,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWalletId = userWalletId, network = currencyToSend.currency.network, ).getOrElse { error -> - Timber.e(error, "Failed to create swap CEX tx data") + TangemLogger.e("Failed to create swap CEX tx data", error) return SwapTransactionState.Error.UnknownError } @@ -2065,7 +2066,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWallet = userWallet, ).getOrNull() ?: error("unable to calculate fee") } catch (e: Exception) { - Timber.e(e, "Failed to get fee") + TangemLogger.e("Failed to get fee", e) // it's impossible next steps without fee return createSwapErrorWith( fromToken = fromTokenStatus, @@ -2491,7 +2492,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this@getQuotesOrEmpty).orEmpty() }.getOrElse { e -> - Timber.e(e, "Failed to get quotes: ${e.message.orEmpty()}") + TangemLogger.e("Failed to get quotes: ${e.message.orEmpty()}", e) emptySet() } } @@ -2505,7 +2506,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return try { SolanaTransactionHelper.removeSignaturesPlaceholders(hash) } catch (e: Exception) { - Timber.e("Failed to format the hash: ${e.message.orEmpty()}") + TangemLogger.e("Failed to format the hash: ${e.message.orEmpty()}") hash } } diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 8a54dbba3d..297b57d054 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -94,7 +94,6 @@ dependencies { implementation(deps.compose.accompanist.systemUiController) implementation(deps.kotlin.serialization) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.decompose.ext.compose) /** Tangem libs */ diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 89634ef7cc..6f5e860827 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -34,10 +34,10 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent import com.tangem.utils.extensions.isZero +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber import java.math.BigDecimal @Suppress("UnusedPrivateMember") @@ -130,7 +130,7 @@ internal class DefaultSwapComponent @AssistedInject constructor( LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) { if (shouldHideBlock) { - Timber.e( + TangemLogger.e( "Dismissing fee selector: " + "shouldHideBlock = $shouldHideBlock, amount = ${dataState.amount}, " + "isInsufficientFunds = ${model.uiState.isInsufficientFunds}", @@ -140,13 +140,13 @@ internal class DefaultSwapComponent @AssistedInject constructor( } val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run { - Timber.e("Dismissing fee selector: fromCryptoCurrency is null") + TangemLogger.e("Dismissing fee selector: fromCryptoCurrency is null") slotNavigation.dismiss() return@LaunchedEffect } val feeCurrencyStatus = feePaidCryptoCurrency ?: run { - Timber.e("Dismissing fee selector: feePaidCryptoCurrency is null") + TangemLogger.e("Dismissing fee selector: feePaidCryptoCurrency is null") slotNavigation.dismiss() return@LaunchedEffect } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 03c229e5c6..05b8f74f9a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -115,7 +115,7 @@ import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -554,12 +554,12 @@ internal class SwapModel @Inject constructor( if (fromCryptoCurrency != null) { updateFeePaidCryptoCurrencyFor(fromCryptoCurrency) } else { - Timber.e("updateFeePaidCryptoCurrencyFor failed: fromCryptoCurrency is null") + TangemLogger.e("updateFeePaidCryptoCurrencyFor failed: fromCryptoCurrency is null") } subscribeToCoinBalanceUpdatesIfNeeded() }.onFailure { error -> - Timber.e(error) + TangemLogger.e("Error", error) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, @@ -601,7 +601,7 @@ internal class SwapModel @Inject constructor( swapRouter.back() }.onFailure { error -> - Timber.e(error) + TangemLogger.e("Error", error) } } } @@ -768,10 +768,10 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, cryptoCurrencyStatus = fromToken, ) - .onLeft { Timber.e("Unable to get fee paid crypto currency status for ${fromToken.currency.id}") } + .onLeft { TangemLogger.e("Unable to get fee paid crypto currency status for ${fromToken.currency.id}") } .onRight { currencyStatus -> if (currencyStatus == null) { - Timber.e("Fee paid crypto currency status is null for ${fromToken.currency.id}") + TangemLogger.e("Fee paid crypto currency status is null for ${fromToken.currency.id}") } } .getOrNull(), @@ -831,11 +831,11 @@ internal class SwapModel @Inject constructor( } } else { feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) - Timber.e("Accidentally empty quotes list") + TangemLogger.e("Accidentally empty quotes list") } }, onError = { error -> - Timber.e("Error when loading quotes: $error") + TangemLogger.e("Error when loading quotes: $error") feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, @@ -1042,14 +1042,14 @@ internal class SwapModel @Inject constructor( val provider = requireNotNull(dataState.selectedProvider) { "Selected provider is null" } val lastLoadedQuotesState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState if (lastLoadedQuotesState == null) { - Timber.e("Last loaded quotes state is null") + TangemLogger.e("Last loaded quotes state is null") return } val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { - Timber.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}") + TangemLogger.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}") showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { delay(SWAP_IN_PROGRESS_DELAY) @@ -1076,7 +1076,7 @@ internal class SwapModel @Inject constructor( when (swapTransactionState) { is SwapTransactionState.TxSent -> { if (fee == null) { - Timber.e("onSwapClick: onSuccess: fee is null after swap") + TangemLogger.e("onSwapClick: onSuccess: fee is null after swap") showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } @@ -1088,7 +1088,7 @@ internal class SwapModel @Inject constructor( txHash = swapTransactionState.txHash, currency = fromCurrency.currency, ).getOrElse { - Timber.i("tx hash explore not supported") + TangemLogger.i("tx hash explore not supported") "" } @@ -1132,7 +1132,7 @@ internal class SwapModel @Inject constructor( } } }.onFailure { error -> - Timber.e(error) + TangemLogger.e("Error", error) startLoadingQuotesFromLastState() showAlert() } @@ -1227,7 +1227,7 @@ internal class SwapModel @Inject constructor( val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) - Timber.e("Fee should not be Empty") + TangemLogger.e("Fee should not be Empty") return@launch } is TxFeeState.MultipleFeeState -> fee.priorityFee @@ -1267,7 +1267,7 @@ internal class SwapModel @Inject constructor( } }.onFailure { showAlert() } }.onFailure { error -> - Timber.e(error.message.orEmpty()) + TangemLogger.e(error.message.orEmpty()) showAlert() } } @@ -1449,14 +1449,14 @@ internal class SwapModel @Inject constructor( coin: CryptoCurrency.Coin, isFromCurrency: Boolean, ) { - Timber.d("Subscribe to ${coin.id} balance updates") + TangemLogger.d("Subscribe to ${coin.id} balance updates") getAccountCurrencyStatusUseCase( userWalletId = userWalletId, currency = coin, ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes .onEach { (account, currencyStatus) -> - Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") + TangemLogger.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") if (isFromCurrency) { dataState = dataState.copy( @@ -2310,7 +2310,7 @@ internal class SwapModel @Inject constructor( val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content if (feeStateUM == null) { - Timber.e("getSelectedFeeState: FeeSelectorUM is not Content: $feeStateUM, returning Legacy state") + TangemLogger.e("getSelectedFeeState: FeeSelectorUM is not Content: $feeStateUM, returning Legacy state") return TxFeeSealedState.Legacy( txFeeState = TxFeeState.Empty, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, @@ -2332,7 +2332,7 @@ internal class SwapModel @Inject constructor( val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content if (feeStateUM == null) { - Timber.e("getSelectedFee: FeeSelectorUM is not Content: $feeStateUM, returning null") + TangemLogger.e("getSelectedFee: FeeSelectorUM is not Content: $feeStateUM, returning null") return null } @@ -2391,7 +2391,7 @@ internal class SwapModel @Inject constructor( if (newState is FeeSelectorUM.Error) { modelScope.launch { - Timber.e("onResult: FeeSelectorUM is Error, isHidden = true") + TangemLogger.e("onResult: FeeSelectorUM is Error, isHidden = true") forceUpdateState.emit(newState.copy(isHidden = true)) } return @@ -2406,7 +2406,7 @@ internal class SwapModel @Inject constructor( newState.feeExtraInfo.feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin if (isFeeCurrencySameAsFromCurrency || isCoinFeeSelected) { - Timber.e("onResult: Fee currency is same as from currency or coin fee selected, reloading quotes") + TangemLogger.e("onResult: Fee currency is same as from currency or coin fee selected, reloading quotes") // block swap button until fee is loaded uiState = uiState.copy( @@ -2430,19 +2430,19 @@ internal class SwapModel @Inject constructor( } override suspend fun loadFee(): Either { - Timber.e("loadFee: Start loading fee") + TangemLogger.e("loadFee: Start loading fee") val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!! if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) { - Timber.e("loadFee: Quotes not loaded ${dataState.lastLoadedSwapStates[selectedProvider]}") + TangemLogger.e("loadFee: Quotes not loaded ${dataState.lastLoadedSwapStates[selectedProvider]}") return Either.Left(GetFeeError.UnknownError) } if (isPermissionNotificationShown()) { - Timber.e("loadFee: Permission notification is shown, cannot load fee") + TangemLogger.e("loadFee: Permission notification is shown, cannot load fee") return Either.Left(GetFeeError.UnknownError) } @@ -2456,10 +2456,10 @@ internal class SwapModel @Inject constructor( reduceBalanceBy = lastReducedBalanceBy.value, ) .onLeft { - Timber.e("loadFee: Failed to load fee with error $it") + TangemLogger.e("loadFee: Failed to load fee with error $it") } .onRight { - Timber.e("loadFee: Fee loaded successfully") + TangemLogger.e("loadFee: Fee loaded successfully") } } diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts index cc18ae7c58..0aee66703c 100644 --- a/features/tangempay/details/impl/build.gradle.kts +++ b/features/tangempay/details/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.error) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Common */ implementation(projects.common.ui) @@ -60,5 +61,4 @@ dependencies { /** Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt index 634b5a0e69..de7acccebe 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -57,7 +57,7 @@ internal class TangemPayChangePinModel @Inject constructor( pin = uiState.value.pinCode, ).getOrNull() } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } uiState.update { it.copy(submitButtonLoading = false) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 4ada6b4b01..cc3b8be977 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -62,7 +62,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -181,7 +181,7 @@ internal class TangemPayDetailsModel @Inject constructor( val result = try { cardDetailsRepository.freezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } result @@ -220,7 +220,7 @@ internal class TangemPayDetailsModel @Inject constructor( val result = try { cardDetailsRepository.unfreezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } result @@ -333,7 +333,7 @@ internal class TangemPayDetailsModel @Inject constructor( val result = try { cardDetailsRepository.getCardBalance(params.userWalletId).onRight { balance = it } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } uiState.update( @@ -357,7 +357,7 @@ internal class TangemPayDetailsModel @Inject constructor( val isDone = try { cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } uiState.update( @@ -404,7 +404,7 @@ internal class TangemPayDetailsModel @Inject constructor( try { cardDetailsRepository.setAddToWalletAsDone(params.userWalletId) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) } uiState.update( transformer = DetailsAddToWalletBannerTransformer( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt index 22e3cf50fe..51aa84aae6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt @@ -3,7 +3,7 @@ package com.tangem.features.tangempay.utils import android.content.Context import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "GoogleWalletUtil" @@ -24,15 +24,16 @@ internal class GoogleWalletUtil @Inject constructor( } private fun getWalletIntent(): Intent? { - return if (walletIntent != null) { - walletIntent + val cached = walletIntent + return if (cached != null) { + cached } else { try { context.packageManager.getLaunchIntentForPackage(WALLET_PACKAGE_NAME) ?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } .also { walletIntent = it } } catch (exception: Exception) { - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) null } } diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts index 15fb515b8b..3ae2de80dd 100644 --- a/features/tangempay/main/api/build.gradle.kts +++ b/features/tangempay/main/api/build.gradle.kts @@ -15,4 +15,6 @@ dependencies { /** Compose */ implementation(deps.compose.runtime) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) } \ No newline at end of file diff --git a/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt new file mode 100644 index 0000000000..a38b7fca5d --- /dev/null +++ b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt @@ -0,0 +1,19 @@ +package com.tangem.features.tangempay.component + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.features.tangempay.entity.TangemPayMainUM + +@Stable +interface TangemPayMainBlockComponent { + + fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt new file mode 100644 index 0000000000..977ce0927e --- /dev/null +++ b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt @@ -0,0 +1,26 @@ +package com.tangem.features.tangempay.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +sealed class TangemPayMainUM { + + data object Empty : TangemPayMainUM() + data object Loading : TangemPayMainUM() + data class UnderReview(val subtitle: TextReference, val onClick: () -> Unit) : TangemPayMainUM() + data class IssuingCard(val onClick: () -> Unit) : TangemPayMainUM() + data class FailedToIssue(val onClick: () -> Unit) : TangemPayMainUM() + data class Content( + val subtitle: TextReference, + val isBalanceFlickering: Boolean, + val balance: TextReference, + val balanceSubtitle: TextReference, + val onClick: () -> Unit, + val shouldShowOnlyCacheWarning: Boolean, + ) : TangemPayMainUM() + + data object TemporaryUnavailable : TangemPayMainUM() + data object SyncNeeded : TangemPayMainUM() + data object ExposedDevice : TangemPayMainUM() +} \ No newline at end of file diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts index eb442c8f69..c1aeff44a2 100644 --- a/features/tangempay/main/impl/build.gradle.kts +++ b/features/tangempay/main/impl/build.gradle.kts @@ -15,10 +15,9 @@ dependencies { /** Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.configToggles) /** Features api */ - implementation(projects.features.tangempay.details.api) + implementation(projects.features.tangempay.main.api) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt new file mode 100644 index 0000000000..716d22e5f7 --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.tangempay.component + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.ui.TangemPayMainBlockItem +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +private const val TANGEM_PAY_ACCOUNT_CONTENT_TYPE = "TangemPayAccount" + +@Suppress("UnusedPrivateProperty") +internal class DefaultTangemPayMainBlockComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: Unit, +) : TangemPayMainBlockComponent, AppComponentContext by context { + + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + item( + key = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, + contentType = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, + ) { + TangemPayMainBlockItem(state, isBalanceHidden, modifier) + } + } + + @AssistedFactory + interface Factory : TangemPayMainBlockComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultTangemPayMainBlockComponent + } +} \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt new file mode 100644 index 0000000000..c7dc8b22ab --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt @@ -0,0 +1,17 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.component.DefaultTangemPayMainBlockComponent +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayMainModule { + @Binds + fun bindTangemPayMainBlockComponent( + factory: DefaultTangemPayMainBlockComponent.Factory, + ): TangemPayMainBlockComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt new file mode 100644 index 0000000000..e95dcc262b --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt @@ -0,0 +1,355 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +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.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.main.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN + +private const val DISABLED_ALPHA = 0.6F + +@Composable +internal fun TangemPayMainBlockItem(state: TangemPayMainUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TangemPayMainUM.Empty -> Unit + is TangemPayMainUM.Loading -> TangemPayMainLoadingItem(modifier) + is TangemPayMainUM.UnderReview -> TangemPayMainUnderReviewItem(state, modifier) + is TangemPayMainUM.IssuingCard -> TangemPayMainIssuingCardItem(state, modifier) + is TangemPayMainUM.FailedToIssue -> TangemPayMainFailedIssueItem(state, modifier) + is TangemPayMainUM.Content -> TangemPayMainBlockContent(state, isBalanceHidden, modifier) + is TangemPayMainUM.TemporaryUnavailable -> TangemPayMainTempUnavailableItem(modifier) + is TangemPayMainUM.SyncNeeded -> TangemPayMainSyncNeededItem(modifier) + is TangemPayMainUM.ExposedDevice -> TangemPayMainExposedDeviceItem(modifier) + } +} + +@Composable +private fun TangemPayMainBlockContent( + state: TangemPayMainUM.Content, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + onClick = state.onClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Image( + painter = painterResource(R.drawable.img_visa_36), + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResourceSafe(R.string.tangempay_payment_account), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.End, + ) { + TangemPayFiatAmount( + text = state.balance.resolveReference(), + isBalanceFlickering = state.isBalanceFlickering, + isBalanceFromCache = state.shouldShowOnlyCacheWarning, + isBalanceHidden = isBalanceHidden, + ) + Text( + text = state.balanceSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } + } +} + +@Composable +private fun TangemPayFiatAmount( + text: String, + isBalanceFlickering: Boolean, + isBalanceFromCache: Boolean, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility(isBalanceFromCache) { + Row( + modifier = Modifier.padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + modifier = Modifier.size(12.dp), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } + } + + Text( + text = text.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.body2.applyBladeBrush( + isEnabled = isBalanceFlickering, + textColor = TangemTheme.colors.text.primary1, + ), + textAlign = TextAlign.End, + ) + } +} + +@Composable +private fun TangemPayMainUnderReviewItem(state: TangemPayMainUM.UnderReview, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = state.subtitle, + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainTempUnavailableItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = TextReference.Str(DASH_SIGN), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainIssuingCardItem(state: TangemPayMainUM.IssuingCard, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_issuing_your_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainFailedIssueItem(state: TangemPayMainUM.FailedToIssue, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = TextReference.Res(R.string.tangempay_payment_account), + caption = TextReference.Res(R.string.tangempay_failed_to_issue_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = com.tangem.core.ui.R.drawable.img_visa_36, + iconEndRes = R.drawable.ic_alert_24, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainSyncNeededItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier.padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_payment_account_sync_needed), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainExposedDeviceItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary) + .alpha(DISABLED_ALPHA), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainLoadingItem(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer(modifier = Modifier.size(36.dp)) + Column( + modifier = Modifier.padding(start = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 70.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 52.dp, minHeight = 12.dp), + ) + } + SpacerWMax() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +private fun TangemPayMainItemsPreview( + @PreviewParameter(TangemPayMainUMPreviewParameterProvider::class) + state: TangemPayMainUM, +) { + TangemThemePreview { + TangemPayMainBlockItem(state = state, isBalanceHidden = false) + } +} + +private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + TangemPayMainUM.Loading, + TangemPayMainUM.SyncNeeded, + TangemPayMainUM.TemporaryUnavailable, + TangemPayMainUM.ExposedDevice, + TangemPayMainUM.FailedToIssue(onClick = {}), + TangemPayMainUM.UnderReview(subtitle = resourceReference(R.string.tangempay_kyc_in_progress), onClick = {}), + TangemPayMainUM.IssuingCard(onClick = {}), + TangemPayMainUM.Content( + subtitle = TextReference.Str("*1234"), + isBalanceFlickering = true, + balance = TextReference.Str("$ 101.56"), + balanceSubtitle = TextReference.Str("USDC"), + onClick = {}, + shouldShowOnlyCacheWarning = true, + ), + ), +) \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index c94b59ab29..5762f030f4 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(projects.core.error) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Common */ implementation(projects.common.routing) @@ -49,7 +50,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other */ - implementation(deps.timber) implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index dadd979f7f..78a7228a9d 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -25,11 +25,11 @@ import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButto import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject import com.tangem.utils.transformer.update as transformerUpdate @@ -100,7 +100,7 @@ internal class TangemPayOnboardingModel @Inject constructor( if (customerInfo.productInstance == null) { repository.createOrder(userWalletId) .onLeft { error -> - Timber.e("Error creating order before KYC: $error") + TangemLogger.e("Error creating order before KYC: $error") } } when (params) { @@ -170,14 +170,14 @@ internal class TangemPayOnboardingModel @Inject constructor( val result = produceInitialDataUseCase(userWalletId) if (result.isLeft()) { val errorMessage = result.leftOrNull()?.message ?: "Unknown error" - Timber.e("Error producing initial data: $errorMessage") + TangemLogger.e("Error producing initial data: $errorMessage") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } repository.getCustomerInfo(userWalletId = userWalletId) .fold( ifLeft = { error -> - Timber.e("Error getCustomerInfo: ${error.errorCode}") + TangemLogger.e("Error getCustomerInfo: ${error.errorCode}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) }, ifRight = { customerInfo -> @@ -187,7 +187,7 @@ internal class TangemPayOnboardingModel @Inject constructor( if (customerInfo.productInstance == null) { repository.createOrder(userWalletId) .onLeft { error -> - Timber.e("Error creating order before KYC: $error") + TangemLogger.e("Error creating order before KYC: $error") } } openKyc(userWalletId) diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 6554b8a54f..3c61c2ed70 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { /** Other libraries */ implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.surveysparrow) /** Core modules */ diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index f19b69a825..bad48fc458 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -26,7 +26,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleAppThemeUM -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.File @OptIn(ExperimentalFoundationApi::class) @@ -98,7 +98,7 @@ private fun Activity.shareFile(file: File?) { ContextCompat.startActivity(this, chooserIntent, null) } catch (ex: Exception) { - Timber.e("Failed to share file: $ex") + TangemLogger.e("Failed to share file: $ex") } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index 599f82475f..5eeb879082 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @HiltViewModel @@ -86,7 +86,7 @@ internal class TesterActionsViewModel @Inject constructor( AppThemeMode.FOLLOW_SYSTEM -> AppThemeMode.FORCE_DARK } - Timber.d( + TangemLogger.d( """ Change app theme mode |- Current theme mode: $currentAppThemeMode @@ -95,7 +95,7 @@ internal class TesterActionsViewModel @Inject constructor( ) changeAppThemeModeUseCase(newAppThemeMode).onLeft { error -> - Timber.e( + TangemLogger.e( """ Unable to change app theme mode |- Error: $error @@ -108,7 +108,7 @@ internal class TesterActionsViewModel @Inject constructor( getAppThemeModeUseCase() .distinctUntilChanged() .onEach { maybeAppThemeMode -> - Timber.d( + TangemLogger.d( """ Current app theme mode updated |- Previous app theme mode: ${uiState.toggleAppThemeUM.currentAppTheme} @@ -119,7 +119,7 @@ internal class TesterActionsViewModel @Inject constructor( uiState = uiState.copy( toggleAppThemeUM = uiState.toggleAppThemeUM.copy( currentAppTheme = maybeAppThemeMode.getOrElse { error -> - Timber.e( + TangemLogger.e( """ Unable to get current app theme mode, using default |- Default theme mode: ${AppThemeMode.DEFAULT} diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt index 5a91649f19..3b2385900c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tester.presentation.surveysparrow import android.app.Activity import com.surveysparrow.ss_android_sdk.SsSurvey import com.surveysparrow.ss_android_sdk.SurveySparrow -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Manager for Survey Sparrow SDK. @@ -33,7 +33,7 @@ class SurveySparrowManager( SurveySparrow(activity, survey) } catch (e: Exception) { - Timber.e(e, "Failed to create SurveySparrow survey") + TangemLogger.e("Failed to create SurveySparrow survey", e) null } } @@ -49,7 +49,7 @@ class SurveySparrowManager( val surveySparrow = createSurvey(activity, customVariables) if (surveySparrow != null) { surveySparrow.startSurveyForResult(requestCode) - Timber.d("SurveySparrow survey started with requestCode: $requestCode") + TangemLogger.d("SurveySparrow survey started with requestCode: $requestCode") } } } \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts index b1b8d7b048..ddd6c0d9f9 100644 --- a/features/token-recieve/impl/build.gradle.kts +++ b/features/token-recieve/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.compose.ui.utils) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.lifecycle.compose) implementation(deps.kotlin.serialization) implementation(deps.decompose.ext.compose) diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 3b4adc992f..51a9203ba8 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(deps.reKotlin) implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) - implementation(deps.timber) implementation(deps.lifecycle.compose) implementation(deps.kotlin.serialization) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index 20cf144c6d..d375886c77 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -31,7 +31,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( @@ -66,18 +66,18 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( val userWallet = userWalletId?.let { getUserWalletUseCase(userWalletId) }?.getOrNull() // If wallet to select is null or locked, ignore deeplink if (userWallet == null || userWallet.isLocked) { - Timber.e("Error on getting user wallet") + TangemLogger.e("Error on getting user wallet") return@launch } if (selectWalletUseCase(userWalletId).getOrNull() == null) { - Timber.e("Error on selecting user wallet") + TangemLogger.e("Error on selecting user wallet") return@launch } val cryptoCurrency = findCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId) if (cryptoCurrency == null) { - Timber.e( + TangemLogger.e( """ Could not get crypto currency for |- $NETWORK_ID_KEY: $networkId diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index fb19d4ef8f..bedcc551ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import arrow.core.merge import arrow.core.right +import com.tangem.utils.logging.TangemLogger import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss @@ -103,7 +104,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration") @@ -499,11 +499,9 @@ internal class TokenDetailsModel @Inject constructor( ), ) - Timber.e( - /* t = */ throwable, - /* message = */ "Unable to get wallet manager for user wallet %s and network %s", - /* ...args = */ userWalletId, - cryptoCurrency.network, + TangemLogger.e( + "Unable to get wallet manager for user wallet $userWalletId and network ${cryptoCurrency.network}", + throwable, ) false @@ -655,7 +653,7 @@ internal class TokenDetailsModel @Inject constructor( cryptoCurrency.network, ).fold( ifLeft = { throwable -> - Timber.e(throwable.cause?.localizedMessage.orEmpty()) + TangemLogger.e(throwable.cause?.localizedMessage.orEmpty()) "" }, ifRight = { it }, @@ -756,12 +754,12 @@ internal class TokenDetailsModel @Inject constructor( val accountId = account?.accountId if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") + TangemLogger.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") return@launch } manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } .onRight { router.popBackStack() } } } @@ -830,7 +828,7 @@ internal class TokenDetailsModel @Inject constructor( txHash = txHash, currency = cryptoCurrency, ).fold( - ifLeft = { Timber.e(it.toString()) }, + ifLeft = { TangemLogger.e(it.toString()) }, ifRight = { router.openUrl(url = it) }, ) } @@ -960,7 +958,7 @@ internal class TokenDetailsModel @Inject constructor( } if (message != null) { internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(message)) - Timber.e(message) + TangemLogger.e(message) } }, ifRight = { @@ -1033,7 +1031,7 @@ internal class TokenDetailsModel @Inject constructor( internalUiState.value = stateFactory.getStateWithErrorDialog( stringReference(e.message.orEmpty()), ) - Timber.e(e.message) + TangemLogger.e("Error: $e") }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() @@ -1068,7 +1066,7 @@ internal class TokenDetailsModel @Inject constructor( internalUiState.value = stateFactory.getStateWithErrorDialog( stringReference(e.message.orEmpty()), ) - Timber.e(e.message) + TangemLogger.e("Error: $e") } } }, @@ -1141,7 +1139,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun showStakingUnavailable() { - Timber.e("Staking is unavailable for ${cryptoCurrency.name}") + TangemLogger.e("Staking is unavailable for ${cryptoCurrency.name}") uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title))) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 38588b2f69..a57f83eafb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -22,7 +22,7 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import kotlin.String @@ -150,7 +150,7 @@ internal class TokenDetailsNotificationConverter( maxManaBalanceAmount = warning.maxAmount?.let { formatMana(it) } ?: run { - Timber.e("FeeResource maxAmount cannot be null in Koinos. Check KoinosWalletManager") + TangemLogger.e("FeeResource maxAmount cannot be null in Koinos. Check KoinosWalletManager") "" }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 4d2e2e0835..61415883f5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -24,7 +24,7 @@ import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal internal class TokenDetailsStakingInfoConverter( @@ -46,7 +46,9 @@ internal class TokenDetailsStakingInfoConverter( state: TokenDetailsState, stakingAvailability: StakingAvailability, ): StakingBlockUM? { - Timber.i("Define staking block for [${status.currency.id.value}] with availability:\n$stakingAvailability") + TangemLogger.i( + "Define staking block for [${status.currency.id.value}] with availability:\n$stakingAvailability", + ) return when (stakingAvailability) { StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable StakingAvailability.Unavailable -> null @@ -73,7 +75,7 @@ internal class TokenDetailsStakingInfoConverter( val iconState = state.tokenInfoBlockState.iconState - Timber.i( + TangemLogger.i( """ getStakingInfoBlock: – stakingBalance: ${stakingBalance ?: "null"} diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index c4204e04e1..bc8cbf7110 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -36,7 +36,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale @@ -122,7 +122,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fun updateTxStatus(tx: ExchangeUM, statusModel: ExchangeStatusModel): ExchangeUM { if (tx.activeStatus == statusModel.status && tx.hasLongTime == statusModel.hasLongTime) { - Timber.e("UpdateTxStatus isn't required. Current status isn't changed") + TangemLogger.e("UpdateTxStatus isn't required. Current status isn't changed") return tx } val hasFailed = statusModel.status.isFailed() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 20d13122a3..3bc30b0d7c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class ExchangeStatusFactory @AssistedInject constructor( @@ -127,7 +127,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( type = provider.type, ) } else { - Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + TangemLogger.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") null } @@ -175,7 +175,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( contractAddress = refundContractAddress, networkId = refundNetwork, ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } .getOrNull() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index cbdf9089fa..60d403d38c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import com.tangem.utils.logging.TangemLogger import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState @@ -25,7 +26,6 @@ import dagger.assisted.AssistedInject import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber @Suppress("LongParameterList") internal class OnrampStatusFactory @AssistedInject constructor( @@ -83,8 +83,8 @@ internal class OnrampStatusFactory @AssistedInject constructor( onrampTx } else { getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( - ifLeft = { - Timber.e("Couldn't update onramp status. $it") + ifLeft = { error -> + TangemLogger.e("Couldn't update onramp status. $error") onrampTx }, ifRight = { statusModel -> diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt index d145a7ab35..a8af24a292 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.coroutines.cancellation.CancellationException @Suppress("LongParameterList") @@ -128,7 +128,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( type = provider.type, ) } else { - Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + TangemLogger.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") null } @@ -176,7 +176,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( contractAddress = refundContractAddress, networkId = refundNetwork, ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } .getOrNull() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt index 802663f78a..f164ef18cb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt @@ -25,7 +25,7 @@ import dagger.assisted.AssistedInject import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class TokenDetailsOnrampStatusFactory @AssistedInject constructor( @@ -84,7 +84,7 @@ internal class TokenDetailsOnrampStatusFactory @AssistedInject constructor( } else { getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( ifLeft = { error -> - Timber.e("Couldn't update onramp status. $error") + TangemLogger.e("Couldn't update onramp status. $error") onrampTx }, ifRight = { statusModel -> diff --git a/features/txhistory/impl/build.gradle.kts b/features/txhistory/impl/build.gradle.kts index aae9596318..3ea746b243 100644 --- a/features/txhistory/impl/build.gradle.kts +++ b/features/txhistory/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.utils) implementation(projects.common.routing) implementation(projects.core.configToggles) implementation(projects.core.analytics) @@ -57,5 +58,4 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.decompose.ext.compose) - implementation(deps.timber) } \ No newline at end of file diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index 7476b496da..7d89a3a061 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -27,7 +27,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -213,7 +213,7 @@ internal class TxHistoryModel @Inject constructor( txHash = txHash, currency = params.currency, ).fold( - ifLeft = { Timber.e(it.toString()) }, + ifLeft = { TangemLogger.e(it.toString()) }, ifRight = { urlOpener.openUrl(url = it) }, ) } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index fb82ff618a..634724e099 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.core.analytics.models) implementation(projects.core.datasource) implementation(projects.common.routing) @@ -71,7 +72,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.reKotlin) /** Tangem libraries */ diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt index ef51de94a8..5da7a76ea4 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -103,7 +103,7 @@ internal class RenameWalletModel @Inject constructor( renameWalletUseCase(userWalletId = params.userWalletId, name = newName.text) .onLeft { error -> - Timber.e("Unable to rename wallet: $error") + TangemLogger.e("Unable to rename wallet: $error") showRenameWalletError(error = error, updatedName = newName.text) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 75dac67162..52efecf7c9 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -57,7 +57,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -250,7 +250,7 @@ internal class WalletSettingsModel @Inject constructor( private fun forgetWallet() = modelScope.launch { val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error -> - Timber.e("Unable to delete wallet: $error") + TangemLogger.e("Unable to delete wallet: $error") messageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), @@ -447,7 +447,7 @@ internal class WalletSettingsModel @Inject constructor( -> modelScope.launch { unlockHotWalletContextualUseCase.invoke(hotWalletId) .onLeft { - Timber.e(it, "Unable to unlock wallet with id ${params.userWalletId}") + TangemLogger.e("Unable to unlock wallet with id ${params.userWalletId}", it) } .onRight { action(true) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt index f698a67127..bf3acacb9a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds @@ -38,7 +38,7 @@ internal class AccountListSortingSaver @Inject constructor( .debounce { 3.seconds } .onEach { accountIds -> applyAccountListSortingUseCase.invoke(accountIds).onLeft { - Timber.e("Error while saving account list sorting: $it") + TangemLogger.e("Error while saving account list sorting: $it") } .onRight { analyticsEventHandler.send(WalletSettingsAnalyticEvents.LongtapAccountsOrder()) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index cb23c78346..e3f92c0e20 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(tangemDeps.hot.core) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) - implementation(deps.timber) implementation(deps.firebase.perf) { exclude(group = "com.google.firebase", module = "protolite-well-known-types") exclude(group = "com.google.protobuf", module = "protobuf-javalite") @@ -149,6 +148,7 @@ dependencies { implementation(projects.features.tangempay.details.api) implementation(projects.features.feed.api) implementation(projects.features.promoBanners.api) + implementation(projects.features.tangempay.main.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index a62274f509..22f857a5f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -37,6 +37,7 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import com.tangem.features.send.v2.api.NetworkSelectionComponent import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent @@ -51,6 +52,7 @@ internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, feedEntryComponentFactory: FeedEntryComponent.Factory, + tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, @@ -70,6 +72,12 @@ internal class WalletComponent @AssistedInject constructor( entryRoute = null, ) } + private val tangemPayMainBlockComponent by lazy { + tangemPayMainBlockComponentFactory.create( + context = child("tangemPayMainBlockComponent"), + params = Unit, + ) + } private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy { if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null @@ -218,10 +226,11 @@ internal class WalletComponent @AssistedInject constructor( val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() + val uiState by model.uiState.collectAsStateWithLifecycle() if (designFeatureToggles.isRedesignEnabled) { WalletScreen2( - state = model.uiState.collectAsStateWithLifecycle().value, + state = uiState, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, @@ -234,8 +243,9 @@ internal class WalletComponent @AssistedInject constructor( ) } else { WalletScreen( - state = model.uiState.collectAsStateWithLifecycle().value, + state = uiState, promoBannersBlockComponent = promoBannersBlockComponent, + tangemPayComponent = tangemPayMainBlockComponent, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index e64451ff14..16d2963864 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -6,13 +6,13 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -25,20 +25,20 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.* import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.qrscanning.models.QrResultSource -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.domain.qrscanning.models.ClassifiedQrContent -import com.tangem.domain.qrscanning.models.QrSendTarget -import com.tangem.domain.walletconnect.WcPairService -import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase +import com.tangem.domain.qrscanning.models.ClassifiedQrContent +import com.tangem.domain.qrscanning.models.QrResultSource +import com.tangem.domain.qrscanning.models.QrSendTarget +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase +import com.tangem.domain.walletconnect.WcPairService +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.wallets.usecase.* -import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -61,6 +61,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider @@ -68,7 +69,7 @@ import com.tangem.utils.coroutines.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TANGEM_PAY_UPDATE_INTERVAL = 60_000L @@ -119,6 +120,8 @@ internal class WalletModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val wcPairService: WcPairService, private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val uiMessageSender: UiMessageSender, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -157,7 +160,7 @@ internal class WalletModel @Inject constructor( modelScope.launch { bindRefcodeWithWalletUseCase.retry() - .onLeft { Timber.e("Failed to bind refcode with wallets: $it") } + .onLeft { TangemLogger.e("Failed to bind refcode with wallets: $it") } } } @@ -308,7 +311,7 @@ internal class WalletModel @Inject constructor( val shouldAskNotificationPermissionsViaBs = notificationsRepository.shouldAskNotificationPermissionsViaBs() val shouldShow = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() - Timber.d( + TangemLogger.d( "push BS afterUpdate: $shouldShow," + "isHuaweiDevice $isHuaweiDevice", ) @@ -427,14 +430,17 @@ internal class WalletModel @Inject constructor( updateTangemPayJobHolder.cancel() modelScope.launch { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) while (isActive) { delay(TANGEM_PAY_UPDATE_INTERVAL) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } }.saveIn(updateTangemPayJobHolder) } else { // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } }.launchIn(modelScope) } @@ -454,7 +460,7 @@ internal class WalletModel @Inject constructor( awaitAll( async { refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { - Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") + TangemLogger.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") } }, async { @@ -497,10 +503,10 @@ internal class WalletModel @Inject constructor( } is WalletsUpdateActionResolver.Action.ReorderWallets -> reorderWallets(action) WalletsUpdateActionResolver.Action.EmptyWallets -> { - Timber.w("Wallets list is empty!") + TangemLogger.w("Wallets list is empty!") } is WalletsUpdateActionResolver.Action.Unknown -> { - Timber.w("Unable to perform action: $action") + TangemLogger.w("Unable to perform action: $action") } } } @@ -543,6 +549,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, isMainScreenQrScanningEnabled = walletFeatureToggles.isMainScreenQrScanningEnabled, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -589,6 +596,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) } @@ -610,6 +618,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) } @@ -624,6 +633,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -685,6 +695,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -820,7 +831,7 @@ internal class WalletModel @Inject constructor( setNotificationsEnabledUseCase(userWalletId, true).onRight { notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue) }.onLeft { - Timber.e(it) + TangemLogger.e("Error", it) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index fccfccfd12..501510ac6a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificat import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletType -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -42,7 +42,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( getUpdateContentAction(currentState, wallets, selectedWallet) } - Timber.i("Resolved action: $action") + TangemLogger.i("Resolved action: $action") return action } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1a2d2f0885..efa8fe4574 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.TangemPayEntryPoint +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -30,7 +31,6 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer import kotlinx.coroutines.launch import javax.inject.Inject @@ -77,6 +77,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private val tangemPayEligibilityManager: TangemPayEligibilityManager, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, ) : BaseWalletClickIntents(), TangemPayIntents { override suspend fun onPullToRefresh() { @@ -85,20 +86,28 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( return } tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } override fun onRefreshPayToken(userWallet: UserWallet) { - stateHolder.update(TangemPayRefreshShowProgressTransformer(userWallet.walletId)) + stateHolder.update( + TangemPayRefreshShowProgressTransformer( + userWalletId = userWallet.walletId, + shouldShowProgress = true, + ), + ) modelScope.launch { produceInitialDataTangemPay.invoke(userWallet.walletId) - .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) } + .onRight { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWallet.walletId)) + } .onLeft { stateHolder.update( - transformer = TangemPayRefreshNeededStateTransformer( - userWallet = userWallet, + TangemPayRefreshShowProgressTransformer( userWalletId = userWallet.walletId, - onRefreshClick = { onRefreshPayToken(userWallet) }, + shouldShowProgress = false, ), ) } @@ -267,7 +276,10 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(TangemPayAnalyticsEvents.KycCancelled()) modelScope.launch { tangemPayOnboardingRepository.disableTangemPay(userWalletId) - .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } + .onRight { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) + } .onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index bf79ed8458..92136614c6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.tokens.TokenItemStateConverter.ApySource import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -40,7 +41,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("TooManyFunctions") @@ -131,12 +131,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { val userWalletId = accountId.userWalletId - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - Timber.e( + val userWallet = getUserWalletUseCase(userWalletId).getOrElse { error -> + TangemLogger.e( """ Unable to get user wallet |- ID: $userWalletId - |- Exception: $it + |- Exception: $error """.trimIndent(), ) @@ -287,7 +287,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( txHash = txHash, currency = currency, ).fold( - ifLeft = { Timber.e(it.toString()) }, + ifLeft = { TangemLogger.e(it.toString()) }, ifRight = { router.openUrl(url = it) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index c3b9d8d1f2..5d86ed9512 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.routing.AppRoute.* import com.tangem.common.routing.AppRouter @@ -11,8 +12,8 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic.ButtonSupport import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.review.ReviewManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -53,7 +54,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("TooManyFunctions") @@ -187,7 +187,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( userWalletId = userWallet.walletId, currencies = missedAddressCurrencies, ).fold( - ifLeft = { Timber.e(it, "Failed to derive public keys") }, + ifLeft = { TangemLogger.e("Failed to derive public keys", it) }, ifRight = { fetchCryptoCurrencies(userWalletId = userWallet.walletId, currencies = missedAddressCurrencies) }, @@ -485,7 +485,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( networks = currencies.map(CryptoCurrency::network).toSet(), ), ) - .onLeft { Timber.e("Unable to fetch networks: $it") } + .onLeft { TangemLogger.e("Unable to fetch networks: $it") } }, async { multiQuoteStatusFetcher( @@ -494,7 +494,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( appCurrencyId = null, ), ) - .onLeft { Timber.e("Unable to fetch quotes: $it") } + .onLeft { TangemLogger.e("Unable to fetch quotes: $it") } }, async { val stakingIds = currencies.mapNotNullTo(hashSetOf()) { @@ -507,7 +507,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( stakingIds = stakingIds, ), ) - .onLeft { Timber.e("Unable to fetch yield balances: $it") } + .onLeft { TangemLogger.e("Unable to fetch yield balances: $it") } }, ) .awaitAll() @@ -516,12 +516,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() - return getUserWalletUseCase(userWalletId).getOrElse { - Timber.e( + return getUserWalletUseCase(userWalletId).getOrElse { error -> + TangemLogger.e( """ Unable to get user wallet |- ID: $userWalletId - |- Exception: $it + |- Exception: $error """.trimIndent(), ) @@ -538,7 +538,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( setNotificationsEnabledUseCase(userWalletId, true).onRight { notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue) }.onLeft { - Timber.e(it) + TangemLogger.e("Error", it) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index e2c99b4aa3..0321bc421a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.time.Duration.Companion.seconds @Suppress("LongParameterList") @@ -72,12 +72,12 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( private fun findSelectedWallet(promoCode: String) { getSelectedWalletSyncUseCase().fold( - ifLeft = { - Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") + ifLeft = { error -> + TangemLogger.withTag(LOG_TAG).e("Error on getting user wallet: $error") showAlert(Failed) }, ifRight = { userWallet -> - Timber.tag(LOG_TAG).d("SelectedUserWallet ${userWallet.walletId.stringValue.mask()}") + TangemLogger.withTag(LOG_TAG).d("SelectedUserWallet ${userWallet.walletId.stringValue.mask()}") findBitcoinAddress(userWallet = userWallet, promoCode = promoCode) }, ) @@ -96,24 +96,24 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( }, ) - Timber.tag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}") + TangemLogger.withTag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}") val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId), ) - Timber.tag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}") + TangemLogger.withTag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}") val bitcoinCurrency = cryptoCurrencies?.firstOrNull { it.id.rawNetworkId == Blockchain.Bitcoin.id } - Timber.tag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency") + TangemLogger.withTag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency") val bitcoinStatus = networkStatuses?.firstOrNull { status -> status.network.id == bitcoinCurrency?.network?.id } - Timber.tag(LOG_TAG).d("BitcoinStatus $bitcoinStatus") + TangemLogger.withTag(LOG_TAG).d("BitcoinStatus $bitcoinStatus") if (bitcoinStatus == null) { - Timber.tag(LOG_TAG).d("No bitcoin, bitcoin network status == null") + TangemLogger.withTag(LOG_TAG).d("No bitcoin, bitcoin network status == null") showAlert(NoBitcoinAddress) } else { val networkAddress = when (bitcoinStatus.value) { @@ -126,7 +126,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( ?.defaultAddress?.value if (bitcoinAddress != null) { - Timber.tag(LOG_TAG).d( + TangemLogger.withTag(LOG_TAG).d( "Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}", ) @@ -138,7 +138,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( } else { uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) - Timber.tag(LOG_TAG).d("No Bitcoin address $bitcoinStatus.value") + TangemLogger.withTag(LOG_TAG).d("No Bitcoin address $bitcoinStatus.value") showAlert(NoBitcoinAddress) } } @@ -155,13 +155,15 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( delay(DEFAULT_MESSAGE_SENDER_DELAY) uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) - Timber.tag(LOG_TAG).d("${promoCode.mask()} activation success on address ${bitcoinAddress.mask()}") + TangemLogger.withTag( + LOG_TAG, + ).d("${promoCode.mask()} activation success on address ${bitcoinAddress.mask()}") showAlert(Activated) }.onLeft { error -> delay(DEFAULT_MESSAGE_SENDER_DELAY) uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) - Timber.tag(LOG_TAG).d("${promoCode.mask()} activation failed $error") + TangemLogger.withTag(LOG_TAG).d("${promoCode.mask()} activation failed $error") val alertType = when (error) { ActivatePromoCodeError.ActivationFailed -> Failed ActivatePromoCodeError.InvalidPromoCode -> InvalidPromoCode @@ -203,7 +205,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( @Suppress("NullableToStringCall") private fun saveAndBindRefcode() { scope.launch(dispatchers.default) { - Timber.i("saveAndBindRefcode: refcode = $refcode, campaign = $campaign") + TangemLogger.i("saveAndBindRefcode: refcode = $refcode, campaign = $campaign") if (!refcode.isNullOrBlank()) { val conversionData = AppsFlyerConversionData(refcode = refcode, campaign = campaign) @@ -211,7 +213,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( appsFlyerStore.storeIfAbsent(value = conversionData) bindRefcodeWithWalletUseCase(conversionData) - .onLeft { Timber.e("Failed to bind refcode with wallets: $it") } + .onLeft { TangemLogger.e("Failed to bind refcode with wallets: $it") } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 5485587450..91a230aa1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -217,6 +218,8 @@ internal object WalletScreenPreviewDataLegacy { onClick = {}, ), type = WalletType.Cold, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 63e329b73f..09f8eba96e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -123,6 +123,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.TangemPayUnreachable -> null is WalletNotification.UpgradeHotWalletPromo -> null is WalletNotification.TokenSyncCompleted -> null + is WalletNotification.CreateTangemPayAccount -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 2438cafa5a..e81d00263d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -7,10 +7,10 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -18,6 +18,8 @@ import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -25,7 +27,6 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map - import javax.inject.Inject @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @@ -69,13 +69,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - - val accountStatusListFlow by lazy { - val params = SingleAccountStatusListProducer.Params(userWallet.walletId) - accountDependencies.singleAccountStatusListSupplier(params) - .map { it.totalFiatBalance to it.flattenCurrencies() } - .map { Lce.Content(it) } - } + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) return combine( accountStatusListFlow, @@ -95,9 +90,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), ) { array -> array } .map { array -> - val lceTokens = array[0] as Lce>> - val totalFiatBalance = lceTokens.map { it.first } - val flattenCurrencies = lceTokens.map { it.second } + val accountStatusList = array[0] as AccountStatusList val isReadyToShowRating = array[1] as Boolean val isNeedToBackup = array[2] as Boolean val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus @@ -108,8 +101,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowUpgradeBanner = array[8] as Boolean val closureTimestamp = array[9] as? Long + val flattenCurrencies = accountStatusList.flattenCurrencies() + val paymentAccountStatus = accountStatusList.accountStatuses + .filterIsInstance() + .firstOrNull() + buildList { - addUsedOutdatedDataNotification(totalFiatBalance) + addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance) addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) @@ -162,24 +160,54 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( if (!hasCriticalOrWarning) { addRateTheAppNotification(isReadyToShowRating, clickIntents) } + + // add as last warning + paymentAccountStatus?.let { paymentAccountStatus -> + addTangemPayWarnings( + status = paymentAccountStatus, + userWallet = userWallet, + walletClickIntents = clickIntents, + ) + } }.toImmutableList() } } - private fun MutableList.addUsedOutdatedDataNotification( - totalFiatBalance: Lce, + private fun MutableList.addTangemPayWarnings( + status: AccountStatus.Payment, + userWallet: UserWallet, + walletClickIntents: WalletClickIntents, ) { + val notification = when (status.value) { + is PaymentAccountStatusValue.Error.NotSynced -> WalletNotification.Warning.TangemPayRefreshNeeded( + buttonText = when (userWallet) { + is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) + is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) + }, + onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) }, + shouldShowProgress = false, + ) + is PaymentAccountStatusValue.NotCreated -> WalletNotification.CreateTangemPayAccount( + onClick = { walletClickIntents.onOnboardingBannerClick(userWallet.walletId) }, + onCloseClick = { walletClickIntents.onOnboardingBannerCloseClick(userWallet.walletId) }, + ) + is PaymentAccountStatusValue.Error.Unavailable -> WalletNotification.Warning.TangemPayUnreachable + is PaymentAccountStatusValue.Error.CardIssueFailed, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.IssuingCard, + is PaymentAccountStatusValue.Loaded, + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Locked, + is PaymentAccountStatusValue.UnderReview, + -> null + } + notification?.let(::add) + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { addIf( element = WalletNotification.UsedOutdatedData, - condition = totalFiatBalance.fold( - ifLoading = { - (it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE - }, - ifContent = { - (it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE - }, - ifError = { false }, - ), + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, ) } @@ -254,7 +282,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addInformationalNotifications( userWallet: UserWallet, cardTypesResolver: CardTypesResolver?, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { addIf( @@ -267,7 +295,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addMissingAddressesNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val currencies = flattenCurrencies.getMissingAddressCurrencies() @@ -285,10 +313,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.getMissingAddressCurrencies(): List { - val flattenCurrencies = getOrNull(isPartialContentAccepted = true) ?: return emptyList() - - return flattenCurrencies + private fun List.getMissingAddressCurrencies(): List { + return this .filter { it.value is CryptoCurrencyStatus.MissedDerivation } .map(CryptoCurrencyStatus::currency) } @@ -330,7 +356,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addWarningNotifications( cardTypesResolver: CardTypesResolver?, - flattenCurrencies: Lce>, + flattenCurrencies: List, isNeedToBackup: Boolean, clickIntents: WalletClickIntents, ) { @@ -355,7 +381,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } private fun MutableList.addCloreMigrationNotification( - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return @@ -367,10 +393,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.findCloreCurrency(): CryptoCurrencyStatus? { - val currencies = getOrNull(isPartialContentAccepted = true) ?: return null - - return currencies.find { currencyStatus -> + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return this.find { currencyStatus -> BlockchainUtils.isClore(currencyStatus.currency.network.rawId) } } @@ -388,10 +412,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.hasUnreachableNetworks(): Boolean { - val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false - - return flattenCurrencies.any { it.value is CryptoCurrencyStatus.Unreachable } + private fun List.hasUnreachableNetworks(): Boolean { + return this.any { it.value is CryptoCurrencyStatus.Unreachable } } // Remove in first iteration of yield supply feature @@ -427,7 +449,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, shouldAccessCodeSkipped: Boolean, ) { @@ -437,12 +459,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && !shouldAccessCodeSkipped val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired - - val type = flattenCurrencies.fold( - ifLoading = { return }, - ifContent = { it.getFinishWalletActivationType() }, - ifError = { WalletActivationBannerType.Attention }, - ) + val type = flattenCurrencies.getFinishWalletActivationType() addIf( element = WalletNotification.FinishWalletActivation( @@ -465,15 +482,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private suspend fun MutableList.addUpgradeHotWalletPromoNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, closureTimestamp: Long?, ) { if (userWallet !is UserWallet.Hot) return - val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() - val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } + val hasBalance = flattenCurrencies.any { it.value.amount.orZero().isPositive() } val shouldShow = checkHotWalletUpgradeBannerUseCase( walletId = userWallet.walletId, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt index cb7eb1f324..4f32140696 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt @@ -7,9 +7,9 @@ import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -42,7 +42,7 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor( }, ) } catch (e: IllegalArgumentException) { - Timber.w(e, "Unable to validate signature count: user wallet not found") + TangemLogger.w("Unable to validate signature count: user wallet not found", e) false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index 2af73a2491..a757334ff9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -18,7 +18,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped @@ -58,7 +58,7 @@ internal class OnrampStatusFactory @Inject constructor( if (!onrampTx.activeStatus.isTerminal) { getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( ifLeft = { - Timber.e("Couldn't update onramp status. $it") + TangemLogger.e("Couldn't update onramp status. $it") }, ifRight = { statusModel -> val txId = statusModel.txId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt index 4b0d406ee5..53d32e9062 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -1,17 +1,17 @@ package com.tangem.feature.wallet.presentation.wallet.domain +import com.tangem.utils.logging.TangemLogger import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import timber.log.Timber internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { return this().fold( - ifLeft = { - Timber.e("Impossible to get selected wallet $it") + ifLeft = { error -> + TangemLogger.e("Impossible to get selected wallet $error") null }, ifRight = { it }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt index 10de4aeeb6..b4cae53df1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -45,7 +45,7 @@ internal class WalletContentFetcher @Inject constructor( * (doesn't matter if it is active or not), then skip the update process. */ if (!forceUpdate && savedJobHolder != null && !savedJobHolder.isEmpty()) { - Timber.d("Skip fetching for $userWalletId") + TangemLogger.d("Skip fetching for $userWalletId") return@withContext } @@ -55,7 +55,7 @@ internal class WalletContentFetcher @Inject constructor( * then cancel the previous update. */ if (forceUpdate && savedJobHolder?.isActive == true) { - Timber.d("Cancel old fetching for $userWalletId") + TangemLogger.d("Cancel old fetching for $userWalletId") savedJobHolder.cancel() } @@ -63,7 +63,7 @@ internal class WalletContentFetcher @Inject constructor( JobHolder().also { fetchingJobMap[userWalletId] = it } } - Timber.d("Start fetching for $userWalletId") + TangemLogger.d("Start fetching for $userWalletId") val maybeResult = launch { walletBalanceFetcher( @@ -71,11 +71,11 @@ internal class WalletContentFetcher @Inject constructor( userWalletId = userWalletId, isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), - ).onLeft(Timber::e) + ).onLeft { TangemLogger.e("Error", it) } } .saveInAndJoin(jobHolder) - Timber.d("Finish fetching with result $maybeResult for $userWalletId") + TangemLogger.d("Finish fetching with result $maybeResult for $userWalletId") } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt index c6754b834f..7ead228271 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.copy import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class WalletNameMigrationUseCase( private val userWalletsListRepository: UserWalletsListRepository, @@ -17,13 +17,13 @@ class WalletNameMigrationUseCase( val wallets = userWalletsListRepository.userWalletsSync() val existingNames: MutableSet = mutableSetOf() - wallets.forEach { - val defaultName = it.name + wallets.forEach { wallet -> + val defaultName = wallet.name val suggestedWalletName = suggestedWalletName(defaultName, existingNames) if (defaultName != suggestedWalletName) { - userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) + userWalletsListRepository.saveWithoutLock(wallet.copy(name = suggestedWalletName), canOverride = true) } - Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) + TangemLogger.withTag("Migrated names").e(wallet.walletId.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt index 8965195057..3eb88195ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.wallet.isLocked import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.newSingleThreadContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -45,19 +45,19 @@ internal class WalletScreenContentLoader @Inject constructor( storage.remove(id) loadInternal(userWallet, coroutineScope, isRefresh = true) } else { - Timber.d("$id content loading has already started") + TangemLogger.d("$id content loading has already started") } } } /** Cancel loading by [id] */ fun cancel(id: UserWalletId) { - Timber.d("$id content loading is canceled") + TangemLogger.d("$id content loading is canceled") storage.remove(id) } fun cancelAll() { - Timber.d("All content loading is canceled") + TangemLogger.d("All content loading is canceled") storage.clear() singleBackgroundDispatcher.close() } @@ -69,11 +69,11 @@ internal class WalletScreenContentLoader @Inject constructor( ) if (loader == null) { - Timber.e("Impossible to create loader for $userWallet") + TangemLogger.e("Impossible to create loader for $userWallet") return } - Timber.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") + TangemLogger.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") loader.subscribers .map { it.subscribe(coroutineScope, singleBackgroundDispatcher) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 89bff8d53b..092521153e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -13,7 +13,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -41,7 +41,7 @@ internal class WalletStateController @Inject constructor( } fun update(transformer: WalletScreenStateTransformer) { - Timber.d("Applying: ${transformer::class.simpleName}") + TangemLogger.d("Applying: ${transformer::class.simpleName}") mutableUiState.update(function = transformer::transform) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e3110857f1..77f9417e93 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -151,7 +151,6 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class TangemPayRefreshNeeded( - @DrawableRes private val tangemIcon: Int?, private val onRefreshClick: () -> Unit, private val buttonText: TextReference, private val shouldShowProgress: Boolean, @@ -160,7 +159,7 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), buttonsState = ButtonsState.PrimaryButtonConfig( text = buttonText, - iconResId = tangemIcon, + iconResId = R.drawable.ic_tangem_24, onClick = onRefreshClick, shouldShowProgress = shouldShowProgress, ), @@ -480,4 +479,11 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ), ) + + data class CreateTangemPayAccount(val onClick: () -> Unit, val onCloseClick: () -> Unit) : WalletNotification( + config = NotificationConfig( + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + iconResId = R.drawable.img_tangem_pay_visa_banner, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 12261f3059..9459402f44 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTx import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -24,6 +25,8 @@ internal sealed interface WalletState : WalletStateHolder { abstract val nftState: WalletNFTItemUM abstract val type: WalletType abstract val tangemPayState: TangemPayState + abstract val tangemPayMainUM: TangemPayMainUM + abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -35,6 +38,8 @@ internal sealed interface WalletState : WalletStateHolder { override val nftState: WalletNFTItemUM, override val type: WalletType, override val tangemPayState: TangemPayState, + override val tangemPayMainUM: TangemPayMainUM, + override val isTangemPayRefactorEnabled: Boolean, ) : MultiCurrency() data class Locked( @@ -54,6 +59,8 @@ internal sealed interface WalletState : WalletStateHolder { override val tokensListState = WalletTokensListState.ContentState.Locked override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val tangemPayState: TangemPayState = TangemPayState.Empty + override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty + override val isTangemPayRefactorEnabled: Boolean = false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index ec3d6943be..7cba602ffb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -13,6 +13,7 @@ internal class AddWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -20,6 +21,7 @@ internal class AddWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt index f30c88927b..1b72b3307d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt @@ -5,7 +5,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenSta import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DeleteWalletTransformer( private val selectedWalletIndex: Int, @@ -31,7 +31,7 @@ internal class DeleteWalletTransformer( wallets = (prevState.wallets - deletedWalletState).toImmutableList(), ) else -> { - Timber.e("Wallets does not contain deleted wallet") + TangemLogger.e("Wallets does not contain deleted wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index a92b59ba44..4c669daeed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -28,6 +28,7 @@ internal class InitializeWalletsTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isMainScreenQrScanningEnabled: Boolean = false, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -35,6 +36,7 @@ internal class InitializeWalletsTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index 0c1b198d0c..ed1b3a1b10 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -24,6 +24,7 @@ internal class ReinitializeNewWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -31,6 +32,7 @@ internal class ReinitializeNewWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 6dab085776..00a8977b27 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -20,6 +20,7 @@ internal class ReinitializeWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { @@ -27,6 +28,7 @@ internal class ReinitializeWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt index af3f38b3a4..01563ac966 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt @@ -6,7 +6,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Transformer that renames wallets @@ -51,7 +51,7 @@ internal class RenameWalletsTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, -> { - Timber.e("Impossible to rename wallet in locked state") + TangemLogger.e("Impossible to rename wallet in locked state") prevState } } @@ -63,7 +63,7 @@ internal class RenameWalletsTransformer( prevState.copy(walletsBalanceUM = prevState.walletsBalanceUM.copySealed(name = newName)) } is WalletUM.Locked -> { - Timber.e("Impossible to rename wallet in locked state") + TangemLogger.e("Impossible to rename wallet in locked state") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt index f3dbd5dd9e..977e103f90 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -11,7 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetCryptoCurrencyActionsTransformer( private val tokenActionsState: TokenActionsState, @@ -26,11 +26,11 @@ internal class SetCryptoCurrencyActionsTransformer( prevState.copy(buttons = tokenActionsState.toManageButtons()) } is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load primary currency status for locked wallet") + TangemLogger.w("Impossible to load primary currency status for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load crypto currency actions for multi-currency wallet") + TangemLogger.w("Impossible to load crypto currency actions for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index 9d0afb1eff..510be12367 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetExpressStatusesTransformer( userWalletId: UserWalletId, @@ -47,11 +47,11 @@ internal class SetExpressStatusesTransformer( ) } is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load express statuses for locked wallet") + TangemLogger.w("Impossible to load express statuses for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load express statuses for multi-currency wallet") + TangemLogger.w("Impossible to load express statuses for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index 5efac70c6d..207eb330f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletMarketPriceConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SetPrimaryCurrencyTransformer( @@ -27,11 +27,11 @@ internal class SetPrimaryCurrencyTransformer( ) } is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load primary currency status for locked wallet") + TangemLogger.w("Impossible to load primary currency status for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load primary currency status for multi-currency wallet") + TangemLogger.w("Impossible to load primary currency status for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index d2740b30c4..6bfbef7ab9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal internal class SetTokenListErrorTransformer( @@ -39,11 +39,11 @@ internal class SetTokenListErrorTransformer( ) } is WalletState.MultiCurrency.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") prevState } is WalletState.SingleCurrency -> { - Timber.w("Impossible to load tokens list for single-currency wallet") + TangemLogger.w("Impossible to load tokens list for single-currency wallet") prevState } } @@ -68,7 +68,7 @@ internal class SetTokenListErrorTransformer( ) } is WalletUM.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 8bbaf12cb9..4c3d0689dc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability @@ -8,10 +9,12 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayMainBlockConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger +import com.tangem.features.tangempay.entity.TangemPayMainUM import java.math.BigDecimal internal class SetTokenListTransformer( @@ -25,22 +28,27 @@ internal class SetTokenListTransformer( private val isAccountsModeEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { + private val tangemPayConverter by lazy { + TangemPayMainBlockConverter(tangemPayClickIntents = clickIntents) + } + override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.MultiCurrency.Content -> { prevState.copy( walletCardState = prevState.walletCardState.toLoadedState(), tokensListState = prevState.tokensListState.toLoadedState(), + tangemPayMainUM = prevState.tangemPayMainUM.toLoadedState(), buttons = prevState.enableButtons(), ) } is WalletState.MultiCurrency.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") prevState } is WalletState.SingleCurrency, -> { - Timber.w("Impossible to load tokens list for single-currency wallet") + TangemLogger.w("Impossible to load tokens list for single-currency wallet") prevState } } @@ -56,7 +64,7 @@ internal class SetTokenListTransformer( ) } is WalletUM.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") walletUM } } @@ -97,6 +105,17 @@ internal class SetTokenListTransformer( ).convert(value = this) } + private fun TangemPayMainUM.toLoadedState(): TangemPayMainUM { + val paymentAccountStatus = when (params) { + is TokenConverterParams.Account -> params.accountList.accountStatuses + .filterIsInstance() + .firstOrNull() + is TokenConverterParams.Wallet -> null + } ?: return this + + return tangemPayConverter.convert(paymentAccountStatus) + } + private fun toLoadedState(): WalletTokensListUM { if (params !is TokenConverterParams.Account) { return WalletTokensListUM.Empty( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index 9a2b1164f9..2773d8a536 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryCountErrorTransformer( private val userWallet: UserWallet, @@ -46,7 +46,7 @@ internal class SetTxHistoryCountErrorTransformer( is WalletState.SingleCurrency.Locked, is WalletState.MultiCurrency, -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt index cba6ac874b..bad52e78a8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryCountTransformer( userWalletId: UserWalletId, @@ -24,11 +24,11 @@ internal class SetTxHistoryCountTransformer( ) is WalletState.SingleCurrency.Locked, -> { - Timber.w("Impossible to load transactions history for locked wallet") + TangemLogger.w("Impossible to load transactions history for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } @@ -40,7 +40,7 @@ internal class SetTxHistoryCountTransformer( private fun TxHistoryState.toLoadingState(): TxHistoryState { return if (this is TxHistoryState.Content) { - Timber.d("Load transactions history: $transactionsCount") + TangemLogger.d("Load transactions history: $transactionsCount") copy( contentItems = contentItems.apply { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt index b8af717c52..de0657e030 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -6,7 +6,7 @@ import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryItemsErrorTransformer( userWalletId: UserWalletId, @@ -18,11 +18,11 @@ internal class SetTxHistoryItemsErrorTransformer( return when (prevState) { is WalletState.SingleCurrency.Content -> prevState.copy(txHistoryState = createErrorState()) is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load transactions history for locked wallet") + TangemLogger.w("Impossible to load transactions history for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt index 289daa4514..f13ad18498 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemFlowConverter import kotlinx.coroutines.flow.Flow -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryItemsTransformer( userWallet: UserWallet, @@ -23,11 +23,11 @@ internal class SetTxHistoryItemsTransformer( txHistoryState = prevState.txHistoryState.toContentState(), ) is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load transactions history for locked wallet") + TangemLogger.w("Impossible to load transactions history for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index fde5efd497..9f36ea4e38 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetWarningsTransformer( userWalletId: UserWalletId, @@ -23,7 +23,7 @@ internal class SetWarningsTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, -> { - Timber.w("Impossible to update notifications for locked wallet") + TangemLogger.w("Impossible to update notifications for locked wallet") prevState } } @@ -36,7 +36,7 @@ internal class SetWarningsTransformer( notificationsCarousel = notificationsCarousel, ) is WalletUM.Locked -> { - Timber.w("Impossible to update notifications for locked wallet") + TangemLogger.w("Impossible to update notifications for locked wallet") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index e2c4d1a6c4..b8673b8774 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -18,7 +18,6 @@ internal class TangemPayRefreshNeededStateTransformer( override fun transform(prevState: WalletState): WalletState { val tangemPayState = TangemPayState.RefreshNeeded( notification = TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = when (userWallet) { is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt index b37a6f916f..17ff78b37f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -5,9 +5,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import kotlinx.collections.immutable.toImmutableList internal class TangemPayRefreshShowProgressTransformer( userWalletId: UserWalletId, + private val shouldShowProgress: Boolean, ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -15,11 +17,19 @@ internal class TangemPayRefreshShowProgressTransformer( val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState val refreshNotification = refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState + val newWarnings = prevState.warnings.map { warning -> + if (warning is WalletNotification.Warning.TangemPayRefreshNeeded) { + warning.copy(shouldShowProgress = shouldShowProgress) + } else { + warning + } + } return multiContentState.copy( tangemPayState = refreshNeededState.copy( - notification = refreshNotification.copy(shouldShowProgress = true), + notification = refreshNotification.copy(shouldShowProgress = shouldShowProgress), ), + warnings = newWarnings.toImmutableList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index fcef9841df..6e44868ca7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -11,13 +11,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class UnlockWalletTransformer( private val unlockedWallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -25,6 +26,7 @@ internal class UnlockWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } @@ -65,7 +67,7 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, -> { - Timber.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("Impossible to unlock wallet with not locked state") prevState } } @@ -77,7 +79,7 @@ internal class UnlockWalletTransformer( userWallet = unlockedWallet, ) is WalletUM.Content -> { - Timber.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("Impossible to unlock wallet with not locked state") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 72c403ae19..2b4dd9e457 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class UpdateWalletCardsCountTransformer( private val userWallet: UserWallet, @@ -25,7 +25,7 @@ internal class UpdateWalletCardsCountTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, -> { - Timber.e("Impossible to update wallet cards count for locked wallet") + TangemLogger.e("Impossible to update wallet cards count for locked wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt new file mode 100644 index 0000000000..41b74bc551 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import java.util.Currency + +private const val POLYGON_CHAIN_ID = 137 + +internal class TangemPayMainBlockConverter( + private val tangemPayClickIntents: TangemPayIntents, +) : Converter { + @Suppress("LongMethod", "CyclomaticComplexMethod") + override fun convert(value: AccountStatus.Payment): TangemPayMainUM { + return when (val statusValue = value.value) { + is PaymentAccountStatusValue.Error.CardIssueFailed -> TangemPayMainUM.FailedToIssue( + onClick = { tangemPayClickIntents.onIssuingFailedClicked(statusValue.customerId) }, + ) + is PaymentAccountStatusValue.Error.ExposedDevice -> TangemPayMainUM.ExposedDevice + is PaymentAccountStatusValue.Error.NotSynced -> TangemPayMainUM.SyncNeeded + is PaymentAccountStatusValue.Error.Unavailable -> TangemPayMainUM.TemporaryUnavailable + is PaymentAccountStatusValue.IssuingCard -> TangemPayMainUM.IssuingCard( + onClick = { tangemPayClickIntents.onIssuingCardClicked() }, + ) + is PaymentAccountStatusValue.UnderReview -> TangemPayMainUM.UnderReview( + subtitle = when (statusValue.kycStatus) { + KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) + else -> TextReference.Res(R.string.tangempay_kyc_in_progress) + }, + onClick = { + when (statusValue.kycStatus) { + KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( + userWalletId = value.account.userWalletId, + customerId = statusValue.customerId, + ) + else -> tangemPayClickIntents.onKycProgressClicked(value.account.userWalletId) + } + }, + ) + is PaymentAccountStatusValue.NotCreated -> TangemPayMainUM.Empty + is PaymentAccountStatusValue.Loading -> TangemPayMainUM.Loading + is PaymentAccountStatusValue.Locked -> TangemPayMainUM.Content( + subtitle = stringReference("*${statusValue.lastFourDigits}"), + isBalanceFlickering = statusValue.source == StatusSource.CACHE, + balance = getBalanceText( + currencyCode = statusValue.currencyCode, + balance = statusValue.fiatBalance.availableBalance, + ), + balanceSubtitle = stringReference("USDC"), // TODO hardcode for now + shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, + onClick = { + tangemPayClickIntents.openDetails( + value.account.userWalletId, + TangemPayDetailsConfig( + customerId = statusValue.customerId, + cardId = statusValue.cardId, + isPinSet = statusValue.isPinSet, + cardFrozenState = TangemPayCardFrozenState.Frozen, + cardNumberEnd = statusValue.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) + is PaymentAccountStatusValue.Loaded -> TangemPayMainUM.Content( + subtitle = stringReference("*${statusValue.lastFourDigits}"), + isBalanceFlickering = statusValue.source == StatusSource.CACHE, + balance = getBalanceText( + currencyCode = statusValue.currencyCode, + balance = statusValue.fiatBalance.availableBalance, + ), + balanceSubtitle = stringReference("USDC"), // TODO hardcode for now + shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, + onClick = { + tangemPayClickIntents.openDetails( + value.account.userWalletId, + TangemPayDetailsConfig( + customerId = statusValue.customerId, + cardId = statusValue.cardId, + isPinSet = statusValue.isPinSet, + cardFrozenState = TangemPayCardFrozenState.Unfrozen, + cardNumberEnd = statusValue.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) + } + } + + private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference { + val currency = Currency.getInstance(currencyCode) + val formattedBalance = balance.format { + fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) + } + return stringReference(formattedBalance) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 06d9f69510..cb033c72d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -17,6 +17,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -33,6 +34,7 @@ internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) { fun create(userWallet: UserWallet): WalletState { @@ -82,6 +84,8 @@ internal class WalletLoadingStateFactory( nftState = WalletNFTItemUM.Hidden, type = WalletType.Hot, tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } @@ -96,6 +100,8 @@ internal class WalletLoadingStateFactory( nftState = WalletNFTItemUM.Hidden, type = WalletType.Cold, tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 48aa789257..463dd73e6e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetToken import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** @@ -126,7 +126,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ?: return }, ifError = { e -> - Timber.e("Failed to load token list: $e") + TangemLogger.e("Failed to load token list: $e") stateController.update( SetTokenListErrorTransformer( selectedWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 04f33d8338..cc818aa976 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class TangemPayMainSubscriber @AssistedInject constructor( @@ -67,7 +67,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( } TangemPayCustomerInfoError.UnknownError -> { // hide TangemPay block - Timber.e("Failed when loading main screen TangemPay info: $tangemPayError") + TangemLogger.e("Failed when loading main screen TangemPay info: $tangemPayError") stateController.update( transformer = TangemPayHiddenStateTransformer(userWalletId), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt index 4b48ea2769..7afe494928 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Component for implementation of flow subscription @@ -18,7 +18,7 @@ internal abstract class WalletSubscriber { protected abstract fun create(coroutineScope: CoroutineScope): Flow<*> fun subscribe(coroutineScope: CoroutineScope, dispatchers: CoroutineDispatcher): Job { - Timber.d("Subscribe on ${this::class.simpleName}") + TangemLogger.d("Subscribe on ${this::class.simpleName}") return create(coroutineScope) .flowOn(dispatchers) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 7e0a0747aa..8f842b82cf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -49,7 +49,6 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetCo import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.expressTransactionsItems -import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy @@ -60,6 +59,7 @@ import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.softLayerShadow import com.tangem.core.ui.extensions.stringResourceSafe @@ -84,6 +84,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -92,6 +94,7 @@ import kotlin.math.roundToInt @Composable internal fun WalletScreen( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, promoBannersBlockComponent: ComposableContentComponent? = null, bottomSheetContent: @Composable (() -> Unit), bottomSheetHeaderHeightProvider: () -> Dp, @@ -106,6 +109,7 @@ internal fun WalletScreen( WalletContent( state = state, + tangemPayComponent = tangemPayComponent, walletsListState = walletsListState, snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, @@ -128,6 +132,7 @@ internal fun WalletScreen( @Composable private fun WalletContent( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, @@ -220,18 +225,12 @@ private fun WalletContent( } } - if (selectedWallet is WalletState.MultiCurrency) { - item( - key = "TangemPayMainScreenBlock", - contentType = selectedWallet.tangemPayState::class.java, - ) { - TangemPayMainScreenBlock( - state = selectedWallet.tangemPayState, - isBalanceHidden = state.isHidingMode, - modifier = itemModifier, - ) - } - } + tangemPayItem( + modifier = itemModifier, + state = selectedWallet, + isHidingMode = state.isHidingMode, + tangemPayComponent = tangemPayComponent, + ) (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> walletState.marketPriceBlockState?.let { marketPriceBlockState -> @@ -749,6 +748,25 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi } } +internal fun LazyListScope.tangemPayItem( + state: WalletState, + isHidingMode: Boolean, + tangemPayComponent: TangemPayMainBlockComponent, + modifier: Modifier = Modifier, +) { + if (state !is WalletState.MultiCurrency) return + + if (state.isTangemPayRefactorEnabled) { + with(tangemPayComponent) { + tangemPayMainContent(modifier = modifier, state = state.tangemPayMainUM, isBalanceHidden = isHidingMode) + } + } else { + item(key = "TangemPayMainScreenBlock", contentType = state.tangemPayState::class.java) { + TangemPayMainScreenBlock(modifier = modifier, state = state.tangemPayState, isBalanceHidden = isHidingMode) + } + } +} + @Composable private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { @@ -767,6 +785,14 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, + tangemPayComponent = object : TangemPayMainBlockComponent { + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + } + }, bottomSheetContent = { Text("Markets Content") }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 90fc34c41f..8d058b28bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.CreatePaymentAccountNotification import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -49,6 +52,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + CreatePaymentAccountNotification( + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + onClick = item.onClick, + onCloseClick = item.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), + ) + } else -> { Notification( config = item.config, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index fbf1117370..862b6c770d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @@ -41,7 +42,6 @@ private fun TangemPayMainScreenBlockPreview() { TangemPayMainScreenBlock( state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.home_button_scan), onRefreshClick = {}, shouldShowProgress = false, @@ -49,13 +49,30 @@ private fun TangemPayMainScreenBlockPreview() { ), isBalanceHidden = false, ) + TangemPayMainScreenBlock( + state = TangemPayState.TemporaryUnavailable(WalletNotification.Warning.TangemPayUnreachable), + isBalanceHidden = false, + ) + TangemPayMainScreenBlock( + state = TangemPayState.OnboardingBanner(onClick = {}, closeOnClick = {}), + isBalanceHidden = false, + ) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) + TangemPayMainScreenBlock( + state = TangemPayState.FailedIssue( + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_failed_to_issue_card), + iconRes = R.drawable.ic_alert_24, + onButtonClick = { }, + ), + isBalanceHidden = false, + ) TangemPayMainScreenBlock( Progress( - title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), - description = TextReference.EMPTY, + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_kyc_in_progress), buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), iconRes = R.drawable.ic_promo_kyc_36, onButtonClick = {}, @@ -65,19 +82,8 @@ private fun TangemPayMainScreenBlockPreview() { TangemPayMainScreenBlock( Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - description = TextReference.EMPTY, - buttonText = TextReference.Res(R.string.common_continue), - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = {}, - ), - isBalanceHidden = false, - ) - - TangemPayMainScreenBlock( - Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - description = TextReference.Res(R.string.tangempay_issue_card_notification_description), + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_issuing_your_card), buttonText = TextReference.EMPTY, iconRes = R.drawable.ic_tangem_pay_promo_card_36, onButtonClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt index b3c06f3a28..715a6168a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt @@ -67,7 +67,6 @@ private fun TangemPayRefreshBlockPreview() { TangemPayRefreshBlock( state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access), onRefreshClick = {}, shouldShowProgress = true, diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index 28fd3f1aec..de88d92e2a 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) class DefaultPromoDeeplinkHandlerTest { @@ -83,7 +82,6 @@ class DefaultPromoDeeplinkHandlerTest { messages = mutableListOf() every { uiMessageSender.send(capture(messages)) } just runs - Timber.uprootAll() } @Test diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 48f84373fe..3c857c76b5 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Domain models */ implementation(projects.domain.account) @@ -73,7 +74,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index 0bb89d47c0..bef2fd2b82 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -60,7 +60,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( modelScope.launch { val session = wcSessionsUseCase.findByTopic(topic = topic) if (session == null) { - Timber.e("Can not find WcSession by topic: $topic") + TangemLogger.e("Can not find WcSession by topic: $topic") messageSender.send(SnackbarMessage(stringReference("Can not find WcSession by topic: $topic"))) dismiss() } else { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt index 56a76748db..916d18477d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -9,7 +9,7 @@ import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.ui.sign.accountPortfolioName import kotlinx.collections.immutable.persistentListOf -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.UUID internal object WcConnectionsPreviewData { @@ -24,13 +24,13 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/satoshi.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("Moralis clicked") }, + onClick = { TangemLogger.d("Moralis clicked") }, ), WcConnectedAppInfo( name = "website.com", iconUrl = "$BASE_URL/doge_again_lol.jpg", subtitle = "https://react-app.walletconnect.com/", - onClick = { Timber.d("website.com clicked") }, + onClick = { TangemLogger.d("website.com clicked") }, verifiedState = VerifiedDAppState.Unknown, ), ), @@ -44,7 +44,7 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, ), ), ), @@ -57,14 +57,14 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/satoshi.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("Moralis clicked") }, + onClick = { TangemLogger.d("Moralis clicked") }, ), WcConnectedAppInfo( name = "website.com", iconUrl = "$BASE_URL/doge_again_lol.jpg", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Unknown, - onClick = { Timber.d("website.com clicked") }, + onClick = { TangemLogger.d("website.com clicked") }, ), ), ), @@ -76,7 +76,7 @@ internal object WcConnectionsPreviewData { name = "React app", iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, verifiedState = VerifiedDAppState.Verified {}, ), ), @@ -90,13 +90,13 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/satoshi.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("Moralis clicked") }, + onClick = { TangemLogger.d("Moralis clicked") }, ), WcConnectedAppInfo( name = "website.com", iconUrl = "$BASE_URL/doge_again_lol.jpg", subtitle = "https://react-app.walletconnect.com/", - onClick = { Timber.d("website.com clicked") }, + onClick = { TangemLogger.d("website.com clicked") }, verifiedState = VerifiedDAppState.Unknown, ), ), @@ -110,7 +110,7 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, ), ), ), @@ -137,7 +137,7 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, ) val walletHeader diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt index 5d82c115da..bc3e83159d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt @@ -8,7 +8,7 @@ import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLi import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.net.URLDecoder internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( @@ -24,7 +24,7 @@ internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { wallet -> val decodedWcUri = URLDecoder.decode(wcUri, DEFAULT_CHARSET_NAME) @@ -38,7 +38,7 @@ internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( }, ) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) } } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt index cc68b795ce..4cd6fec245 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.converter +import com.tangem.utils.logging.TangemLogger import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase @@ -8,7 +9,6 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber import javax.inject.Inject internal class WcTransactionRequestBlockUMConverter @Inject constructor( @@ -42,7 +42,7 @@ internal class WcTransactionRequestBlockUMConverter @Inject constructor( try { addAll(transactionParamsConverter.convert(params)) } catch (exception: Exception) { - Timber.e(exception, "Error while parsing transaction params - %s", params) + TangemLogger.e("Error while parsing transaction params - $params", exception) } } } diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 09713985c8..e21ec865c7 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.common.routing) implementation(projects.common.ui) @@ -57,7 +58,6 @@ dependencies { implementation(deps.decompose) implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) implementation(tangemDeps.hot.core) diff --git a/features/yield-supply/impl/build.gradle.kts b/features/yield-supply/impl/build.gradle.kts index ada6f291fb..d71b9cf36a 100644 --- a/features/yield-supply/impl/build.gradle.kts +++ b/features/yield-supply/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.utils) /** Compose */ implementation(tangemDeps.vico.core) @@ -69,7 +70,6 @@ dependencies { /** Other */ implementation(deps.decompose) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.kotlin.immutable.collections) /** DI */ diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index 8ddd253ffb..92b9ebf190 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -40,7 +40,7 @@ import com.tangem.utils.transformer.update import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -202,7 +202,7 @@ internal class YieldSupplyActiveModel @Inject constructor( } }, ifEmpty = { - Timber.w("No currency status found: ${cryptoCurrency.id}") + TangemLogger.w("No currency status found: ${cryptoCurrency.id}") }, ) } @@ -210,7 +210,7 @@ internal class YieldSupplyActiveModel @Inject constructor( .launchIn(modelScope) }, ifLeft = { error -> - Timber.w(error.toString()) + TangemLogger.w(error.toString()) return@launch }, ) @@ -232,7 +232,7 @@ internal class YieldSupplyActiveModel @Inject constructor( apy = TextReference.Str(DASH_SIGN), ) } - Timber.e("Error loading token status") + TangemLogger.e("Error loading token status") } } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt index 5314191db2..649028c56e 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt @@ -16,7 +16,7 @@ import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped @@ -43,7 +43,7 @@ internal class YieldSupplyEntryModel @Inject constructor( singleAccountStatusListSupplier.getSyncOrNull(userWalletId) .getCryptoCurrencyStatus(currency = cryptoCurrency) .onNone { - Timber.e("Failed to get CryptoCurrencyStatus: ${cryptoCurrency.id}") + TangemLogger.e("Failed to get CryptoCurrencyStatus: ${cryptoCurrency.id}") withContext(dispatchers.mainImmediate) { router.pop() } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index ac026fed7c..70194d75cc 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.main.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -35,7 +36,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.properties.Delegates @@ -100,8 +100,8 @@ internal class YieldSupplyModel @Inject constructor( userWallet = wallet subscribeOnCurrencyStatusUpdates() }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + TangemLogger.w(error.toString()) return@launch }, ) @@ -132,7 +132,7 @@ internal class YieldSupplyModel @Inject constructor( onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus) }, ifEmpty = { - Timber.w("Unable to get crypto currency status: ${cryptoCurrency.id}") + TangemLogger.w("Unable to get crypto currency status: ${cryptoCurrency.id}") }, ) } @@ -149,8 +149,8 @@ internal class YieldSupplyModel @Inject constructor( onStartEarningClick = ::onStartEarningClick, ), ) - }.onLeft { - Timber.e(it) + }.onLeft { error -> + TangemLogger.e("Error", error) uiState.update { YieldSupplyUM.Initial } } } @@ -261,7 +261,7 @@ internal class YieldSupplyModel @Inject constructor( } computeAndApplyShowInfoIcon(cryptoCurrencyStatus) }.onLeft { t -> - Timber.e(t) + TangemLogger.e("Error", t) uiState.update { YieldSupplyUM.Content( title = resourceReference( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 08cbe91674..a4b555063f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.subcomponents.approve.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -12,15 +13,18 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase @@ -37,14 +41,12 @@ import com.tangem.features.yield.supply.impl.subcomponents.approve.YieldSupplyAp import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsComponent import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateTrigger import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData -import com.tangem.core.ui.extensions.TextReference import com.tangem.utils.TangemBlogUrlBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -141,7 +143,7 @@ internal class YieldSupplyApproveModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) analyticsEventHandler.send( YieldSupplyAnalytics.EarnErrors( @@ -219,8 +221,8 @@ internal class YieldSupplyApproveModel @Inject constructor( contractAddress = cryptoCurrency.contractAddress, spenderAddress = contractAddress, amount = null, - ).getOrElse { - Timber.e(it) + ).getOrElse { error -> + TangemLogger.e("Error", error) return } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 0e7ff956e2..4824105018 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.subcomponents.startearning.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.TransactionSender import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -14,13 +15,13 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -44,7 +45,6 @@ import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -231,7 +231,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( sendMode = TransactionSender.MultipleTransactionSendMode.DEFAULT, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) analytics.send( YieldSupplyAnalytics.EarnErrors( @@ -323,8 +323,8 @@ internal class YieldSupplyStartEarningModel @Inject constructor( } getCurrenciesStatusUpdates() }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + TangemLogger.w(error.toString()) showAlertError() }, ) @@ -362,7 +362,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( ) }, ifEmpty = { - Timber.w("Unable to get crypto currency status: ${cryptoCurrency.id}") + TangemLogger.w("Unable to get crypto currency status: ${cryptoCurrency.id}") showAlertError() }, ) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index fbee3f5bb8..2d62921de0 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -46,7 +46,7 @@ import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -153,7 +153,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) analytics.send( YieldSupplyAnalytics.EarnErrors( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4cde3e5c09..32ce0df01b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -77,7 +77,6 @@ rekotlin = "1.0.4" retrofit = "2.11.0" retrofitMoshiConverter = "2.9.0" spongycastleCryptoCore = "1.58.0.0" -timber = "4.7.1" kermit = "2.1.0" viewBindingDelegate = "1.5.9" xmlShimmer = "1.1.3" @@ -284,7 +283,6 @@ reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-response-type-keeper = { module = "com.squareup.retrofit2:response-type-keeper", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitMoshiConverter" } -timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 42a75d606f..8e9d2b66df 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 6c0fb03aca..2327fa9623 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -7,7 +7,7 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchainsdk.providers.BlockchainProviderTypes -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -26,7 +26,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { - Timber.i("Create WalletManagerFactory") + TangemLogger.i("Create WalletManagerFactory") return WalletManagerFactory( config = config, diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt index b988b3fce1..ba05506e1a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt @@ -6,7 +6,7 @@ import com.tangem.blockchainsdk.providers.BlockchainProviderTypes import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.utils.converter.TwoWayConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] and vice versa @@ -23,7 +23,7 @@ internal object BlockchainProviderTypesConverter : val providerTypes = ProviderTypeConverter.convertList(input = blockchainProviders) providerTypes.forEach { - if (it == null) Timber.e("$blockchain provider type is not supported") + if (it == null) TangemLogger.e("$blockchain provider type is not supported") } blockchain to providerTypes.filterNotNull() diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index 4c04ab93ee..86a63d3888 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -53,7 +53,7 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( ) }, onFailure = { throwable -> - Timber.e(throwable, "Failed to load blockchain provider types from backend") + TangemLogger.e("Failed to load blockchain provider types from backend", throwable) localResponse.ifEmpty { null } }, ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt index 6e1a918463..575672207a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -5,7 +5,7 @@ import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.local.config.providers.models.ProviderModel -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -81,7 +81,7 @@ internal class BlockchainProvidersResponseMerger @Inject internal constructor( "Remote config does not contain some blockchains or providers information", ) - Timber.e(exception) + TangemLogger.e("Error", exception) analyticsExceptionHandler.sendException( ExceptionAnalyticsEvent( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt index 44587e8923..349870c0c8 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt @@ -8,7 +8,7 @@ import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Implementation of [BlockchainProvidersTypesManager] in DEV environment @@ -36,7 +36,7 @@ internal class DevBlockchainProvidersTypesManager( val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty() if (changed.isEmpty()) { - Timber.i("Initialize ChangedBlockchainProvidersStore") + TangemLogger.i("Initialize ChangedBlockchainProvidersStore") changedBlockchainProvidersStore.updateData { BlockchainProviderTypesConverter.convertBack(initial) } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt index c535a0f61a..d1d0a03326 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt @@ -2,7 +2,7 @@ package com.tangem.blockchainsdk.providers import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter import kotlinx.coroutines.flow.Flow -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -26,11 +26,11 @@ internal class ProdBlockchainProvidersTypesManager @Inject constructor( val response = blockchainProvidersResponseLoader.load() if (response == null) { - Timber.e("Error loading BlockchainProviderTypes") + TangemLogger.e("Error loading BlockchainProviderTypes") return } - Timber.i("Update BlockchainProviderTypes") + TangemLogger.i("Update BlockchainProviderTypes") blockchainProviderTypesStore.store( value = BlockchainProviderTypesConverter.convert(response), diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 4b9238c713..6f903222b3 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -28,7 +28,6 @@ dependencies { // region Other deps implementation(deps.kotlin.coroutines) - implementation(deps.timber) // endregion // region Test libraries diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt index 32e90c0d4b..026215cd7c 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.isUTXO import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.network.Network -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Utility class to recognize the account node in a derivation path based on the blockchain type. @@ -36,7 +36,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { .takeIf { nodesCount >= it } if (index == null) { - Timber.e("Cannot determine account node index for ${blockchain.fullName}: ${derivationPath.rawPath}") + TangemLogger.e("Cannot determine account node index for ${blockchain.fullName}: ${derivationPath.rawPath}") } return index @@ -63,7 +63,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { fun recognize(derivationPath: DerivationPath): Long? { return runCatching { if (!blockchain.isAccountsSupported()) { - Timber.e("Account derivation is not supported for blockchain: ${blockchain.fullName}") + TangemLogger.e("Account derivation is not supported for blockchain: ${blockchain.fullName}") return null } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt index fe1f193f9a..a1d01ad5e5 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt @@ -3,7 +3,7 @@ package com.tangem.lib.crypto.derivation import com.tangem.blockchain.common.Blockchain import com.tangem.crypto.hdWallet.DerivationNode import com.tangem.crypto.hdWallet.DerivationPath -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** Extension function to convert a [DerivationPath] into a [MutableDerivationPath] */ fun DerivationPath.toMutable(): MutableDerivationPath = MutableDerivationPath(value = this) @@ -33,7 +33,7 @@ class MutableDerivationPath internal constructor(val value: DerivationPath) { is DerivationNode.NonHardened -> DerivationNode.NonHardened(value) } } else { - Timber.e("Account node not found in the derivation path: ${this@MutableDerivationPath.value}") + TangemLogger.e("Account node not found in the derivation path: ${this@MutableDerivationPath.value}") } return DerivationPath(path = mutableNodes).toMutable() diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index f780f554c2..bf167d3a08 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { } /** Other libraries */ - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index 25a86d101f..4ae4d03774 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -4,6 +4,6 @@ internal object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 const val minSdkVersion = 24 - const val targetSdkVersion = 35 - const val compileSdkVersion = 35 + const val targetSdkVersion = 36 + const val compileSdkVersion = 36 } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4a3ebd13c0..01dadd7270 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -392,6 +392,7 @@ include(":domain:yield-supply") include(":domain:yield-supply:models") include(":domain:news") include(":domain:earn") +include(":domain:search") // endregion Domain modules // region Data modules @@ -430,4 +431,5 @@ include(":data:wallet-manager") include(":data:yield-supply") include(":data:news") include(":data:earn") +include(":data:search") // endregion Data modules \ No newline at end of file