Updated on 2026-08-14
This commit is contained in:
commit
e52f9de341
987 changed files with 20247 additions and 8171 deletions
471
.claude/docs/navigation-graph.md
Normal file
471
.claude/docs/navigation-graph.md
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
# Navigation Graph
|
||||
|
||||
Complete navigation map of the app based on `AppRoute` sealed class and feature-internal routes.
|
||||
|
||||
## 1. All AppRoute Paths
|
||||
|
||||
58 top-level routes defined in `common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt`.
|
||||
|
||||
| # | Route | Path | Description |
|
||||
|---|-------|------|-------------|
|
||||
| 1 | `Initial` | `/initial` | App entry point (splash) |
|
||||
| 2 | `Home` | `/home` | Stories/home screen with launch mode |
|
||||
| 3 | `Welcome` | `/welcome` | Welcome screen for returning users |
|
||||
| 4 | `Disclaimer` | `/disclaimer` | Terms of service / disclaimer |
|
||||
| 5 | `Wallet` | `/wallet` | Main wallet portfolio screen |
|
||||
| 6 | `CurrencyDetails` | `/currency_details/{walletId}/{currencyId}` | Token/coin detail screen |
|
||||
| 7 | `Send` | `/send/{walletId}/{currencyId}` | Send cryptocurrency |
|
||||
| 8 | `Details` | `/details/{walletId}` | Wallet details / settings hub |
|
||||
| 9 | `DetailsSecurity` | `/details/security` | Security mode settings |
|
||||
| 10 | `Usedesk` | `/usedesk/{walletId}` | Customer support (Usedesk) |
|
||||
| 11 | `CardSettings` | `/card_settings/{walletId}` | Card-specific settings |
|
||||
| 12 | `AppSettings` | `/app_settings` | Global app settings |
|
||||
| 13 | `ResetToFactory` | `/reset_to_factory/{walletId}/{cardId}/...` | Factory reset flow |
|
||||
| 14 | `AccessCodeRecovery` | `/access_code_recovery` | Access code recovery |
|
||||
| 15 | `ManageTokens` | `{source}/manage_tokens/{accountId}` | Add/remove tokens in portfolio |
|
||||
| 16 | `ChooseManagedTokens` | `/{source}/choose_managed_tokens/...` | Token chooser for send-via-swap |
|
||||
| 17 | `WalletConnectSessions` | `/wallet_connect_sessions` | WalletConnect sessions list |
|
||||
| 18 | `QrScanning` | `/{source}/qr_scanning` | QR code scanner |
|
||||
| 19 | `ReferralProgram` | `/referral_program` | Referral program |
|
||||
| 20 | `Swap` | `/swap/{fromId}/{toId}/{walletId}/...` | Token swap screen |
|
||||
| 21 | `AppCurrencySelector` | `/app_currency_selector` | Fiat currency selector |
|
||||
| 22 | `Staking` | `/staking/{walletId}/{currencyId}/{integrationId}` | Staking screen |
|
||||
| 23 | `PushNotification` | `/push_notification` | Push notification opt-in |
|
||||
| 24 | `WalletSettings` | `/wallet_settings/{walletId}` | Per-wallet settings |
|
||||
| 25 | `WalletBackup` | `/wallet_backup/{walletId}/{coldOption}` | Wallet backup options |
|
||||
| 26 | `WalletHardwareBackup` | `/wallet_hardware_backup/{walletId}` | Hardware wallet backup |
|
||||
| 27 | `Markets` | `/markets` | Markets token list |
|
||||
| 28 | `MarketsTokenDetails` | `/markets_token_details/{tokenId}/{showPortfolio}` | Market token detail |
|
||||
| 29 | `Onramp` | `/onramp/{walletId}/{symbol}` | Buy crypto (onramp) |
|
||||
| 30 | `OnrampSuccess` | `/onramp/success/{txId}` | Onramp success screen |
|
||||
| 31 | `BuyCrypto` | `/buy_crypto/{walletId}` | Buy crypto token selector |
|
||||
| 32 | `SellCrypto` | `/sell_crypto/{walletId}` | Sell crypto token selector |
|
||||
| 33 | `SwapCrypto` | `/swap_crypto/{walletId}` | Swap crypto token selector |
|
||||
| 34 | `Onboarding` | `/onboarding_v2/{mode}` | Onboarding flow (v2) |
|
||||
| 35 | `Stories` | `/stories$storyId` | Stories / promotional content |
|
||||
| 36 | `NFT` | `/nft/{walletId}` | NFT collection list |
|
||||
| 37 | `NFTSend` | `/send/nft/{walletId}/{collection}/{assetId}` | Send NFT |
|
||||
| 38 | `CreateWalletSelection` | `/create_wallet_selection` | Choose wallet creation type |
|
||||
| 39 | `CreateWalletStart` | `/create_wallet_start` | Wallet creation intro (cold/hot) |
|
||||
| 40 | `CreateHardwareWallet` | `/create_hardware_wallet` | Create hardware wallet flow |
|
||||
| 41 | `CreateMobileWallet` | `/create_mobile_wallet` | Create mobile (hot) wallet |
|
||||
| 42 | `UpgradeWallet` | `/upgrade_wallet/{walletId}` | Upgrade hot wallet to hardware |
|
||||
| 43 | `AddExistingWallet` | `/add_existing_wallet` | Import existing wallet |
|
||||
| 44 | `WalletActivation` | `/wallet_activation/{walletId}` | Activate wallet post-creation |
|
||||
| 45 | `CreateWalletBackup` | `/create_wallet_backup/{walletId}` | Backup flow for created wallet |
|
||||
| 46 | `UpdateAccessCode` | `/update_access_code/{walletId}` | Change access code |
|
||||
| 47 | `ViewPhrase` | `/view_seed_phrase/{walletId}` | View recovery phrase |
|
||||
| 48 | `ForgetWallet` | `/forget_wallet/{walletId}` | Remove wallet from app |
|
||||
| 49 | `SendEntryPoint` | `/send_entry_point/{walletId}/{currencyId}` | Send entry with swap option |
|
||||
| 50 | `CreateAccount` | `/create_account/{walletId}` | Create new account |
|
||||
| 51 | `EditAccount` | `/edit_account/{accountId}` | Edit account |
|
||||
| 52 | `AccountDetails` | `/account_details/{accountId}` | Account details screen |
|
||||
| 53 | `ArchivedAccountList` | `/archived_account/{walletId}` | Archived accounts list |
|
||||
| 54 | `TangemPayDetails` | `/tangem_pay_details/{walletId}` | Tangem Pay card details |
|
||||
| 55 | `TangemPayOnboarding` | `/tangem_pay_onboarding/{mode}` | Tangem Pay onboarding |
|
||||
| 56 | `Kyc` | `/kyc` | KYC verification |
|
||||
| 57 | `YieldSupplyEntry` | `/yield_supply_entry/{walletId}/{symbol}` | Yield/supply entry point |
|
||||
| 58 | `NewsDetails` | `/news_details/{newsId}` | News article detail |
|
||||
|
||||
## 2. Navigation Edges
|
||||
|
||||
Each entry shows: **Source route** → target routes it can navigate to (via `push` or `replaceAll`).
|
||||
|
||||
### Initial / Bootstrap
|
||||
|
||||
| Source | Target | Method | Trigger |
|
||||
|--------|--------|--------|---------|
|
||||
| `Initial` | `Home`, `Welcome`, `Disclaimer`, `Onboarding`, etc. | `replaceAll` | App startup (DefaultRoutingComponent) |
|
||||
|
||||
### Home
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `ManageTokens(STORIES)` | push | After scan, manage tokens |
|
||||
| `CreateWalletStart` | push | Create wallet from home |
|
||||
| `Wallet` | replaceAll | After wallet saved / already saved |
|
||||
|
||||
### Welcome
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CreateWalletSelection` | push | "Add new wallet" button |
|
||||
| `Home()` | replaceAll | When wallets list becomes empty |
|
||||
| `Wallet` | replaceAll | After scan / wallet unlock / biometric |
|
||||
|
||||
### Disclaimer
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `PushNotification(Stories)` | push | After accepting TOS (stories flow) |
|
||||
| `Home()` | replaceAll | After accepting TOS (non-stories flow) |
|
||||
|
||||
### Wallet (main portfolio)
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Details` | push | Open wallet details |
|
||||
| `ManageTokens(ACCOUNT)` | push | Manage tokens for account |
|
||||
| `Onboarding` | push | Continue backup / onboarding |
|
||||
| `CurrencyDetails` | push | Tap on a token |
|
||||
| `Home` | push | Open stories |
|
||||
| `NFT` | push | Open NFT collection |
|
||||
| `TangemPayOnboarding` | push | Tangem Pay banner |
|
||||
| `TangemPayDetails` | push | Tangem Pay card details |
|
||||
| `YieldSupplyEntry` | push | Yield supply action |
|
||||
| `QrScanning(MainScreen)` | push | QR scanner |
|
||||
| `Send` | push | Send from QR / action |
|
||||
| `WalletBackup` | push | Backup warning banner |
|
||||
|
||||
### CurrencyDetails (token details)
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Onramp` | push | Buy action |
|
||||
| `SendEntryPoint` | push | Send action |
|
||||
| `Swap` | push | Swap action |
|
||||
| `CurrencyDetails` | push | Navigate to related token (from staking router) |
|
||||
| `Staking` | push | Open staking (from token details router) |
|
||||
|
||||
### Details (wallet details hub)
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CreateWalletSelection` | push | Add new wallet |
|
||||
| `WalletSettings` | push | Open wallet settings |
|
||||
| `WalletConnectSessions` | push | WalletConnect item |
|
||||
| `AppSettings` | push | App settings item |
|
||||
| `Disclaimer(isTosAccepted=true)` | push | View TOS |
|
||||
| `Usedesk` | push | Customer support |
|
||||
| `TangemPayOnboarding(FromBannerInSettings)` | push | Tangem Pay banner |
|
||||
|
||||
### WalletSettings
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `ReferralProgram` | push | Referral program |
|
||||
| `WalletHardwareBackup` | push | Hardware backup |
|
||||
| `CardSettings` | push | Card settings |
|
||||
| `ForgetWallet` | push | Delete/forget wallet |
|
||||
| `ViewPhrase` | push | View seed phrase |
|
||||
| `AccountDetails` | push | Open account details |
|
||||
| `ArchivedAccountList` | push | View archived accounts |
|
||||
| `CreateAccount` | push | Create new account |
|
||||
| `Home()` | replaceAll | After wallet deletion completes |
|
||||
|
||||
### WalletBackup
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `WalletActivation` | push | Start activation (no backup) |
|
||||
| `ViewPhrase` | push | View phrase option |
|
||||
| `WalletHardwareBackup` | push | Hardware backup option |
|
||||
|
||||
### WalletHardwareBackup
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CreateHardwareWallet` | push | Create new hardware wallet |
|
||||
| `UpgradeWallet` | push | Upgrade current hot wallet |
|
||||
| `CreateWalletBackup` | push | Backup existing wallet |
|
||||
|
||||
### CreateWalletSelection
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CreateMobileWallet` | push | Choose mobile wallet |
|
||||
| `CreateHardwareWallet` | push | Choose hardware wallet |
|
||||
|
||||
### CreateWalletStart
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CreateMobileWallet` | push | Create mobile wallet |
|
||||
| `Wallet` | replaceAll | After wallet creation completes |
|
||||
|
||||
### CreateMobileWallet
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `AddExistingWallet` | push | Import existing wallet |
|
||||
| `Wallet` | replaceAll | After creation completes |
|
||||
|
||||
### CreateHardwareWallet
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Wallet` | replaceAll | After hardware wallet created |
|
||||
|
||||
### AddExistingWallet
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Wallet` | replaceAll | After import completes |
|
||||
|
||||
### UpgradeWallet
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Onboarding(UpgradeHotWallet)` | push | Start upgrade onboarding |
|
||||
|
||||
### CreateWalletBackup
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `UpgradeWallet` | push | After backup, continue to upgrade |
|
||||
|
||||
### AccountDetails
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `EditAccount` | push | Edit account |
|
||||
|
||||
### ForgetWallet
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Home()` | replaceAll | After wallet forgotten |
|
||||
|
||||
### WalletConnectSessions
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `QrScanning(WalletConnect)` | push | Scan WC QR code |
|
||||
|
||||
### Onboarding
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Home()` | replaceAll | Onboarding completed (no wallets) |
|
||||
| `Wallet` | replaceAll | Onboarding completed (has wallets) |
|
||||
|
||||
### PushNotification
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Home()` | replaceAll | After push notification opt-in (via nextRoute param) |
|
||||
|
||||
### Staking
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CurrencyDetails` | push | Back to token details |
|
||||
|
||||
### TangemPayDetails
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Swap` | push | Top up / withdraw via swap |
|
||||
|
||||
### NFT
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `NFTSend` | push | Send NFT |
|
||||
|
||||
### Send (notifications)
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `CurrencyDetails` | push | Navigate to fee token |
|
||||
|
||||
### SwapCrypto / BuyCrypto / SellCrypto
|
||||
|
||||
| Target | Method | Trigger |
|
||||
|--------|--------|---------|
|
||||
| `Swap` | push | After token selection (SwapCrypto) |
|
||||
| `Onramp` | push | After token selection (BuyCrypto/SellCrypto) |
|
||||
|
||||
### Deep Link Handlers (push to AppRoute)
|
||||
|
||||
| Handler | Target Route |
|
||||
|---------|-------------|
|
||||
| `OnrampDeepLinkHandler` | Processes onramp callback params |
|
||||
| `SellRedirectDeepLinkHandler` | `Send` (with sell redirect params) |
|
||||
| `BuyDeepLinkHandler` | `BuyCrypto` |
|
||||
| `SellDeepLinkHandler` | `SellCrypto` |
|
||||
| `SwapDeepLinkHandler` | `SwapCrypto` |
|
||||
| `ReferralDeepLinkHandler` | Referral handling |
|
||||
| `WalletDeepLinkHandler` | Wallet handling |
|
||||
| `TokenDetailsDeepLinkHandler` | `CurrencyDetails` |
|
||||
| `StakingDeepLinkHandler` | `Staking` |
|
||||
| `MarketsDeepLinkHandler` | `Markets` |
|
||||
| `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` |
|
||||
| `WalletConnectDeepLinkHandler` | WalletConnect pairing |
|
||||
| `PromoDeeplinkHandler` | Promo handling |
|
||||
| `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` |
|
||||
| `NewsDetailsDeepLinkHandler` | `NewsDetails` |
|
||||
|
||||
## 3. Nested Routes (Feature-Internal Navigation)
|
||||
|
||||
### OnboardingRoute
|
||||
**File:** `features/onboarding-v2/impl/.../routing/OnboardingRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `None` | Initial empty state |
|
||||
| `Note` | Single-card onboarding note |
|
||||
| `MultiWallet` | Multi-wallet onboarding (with seed phrase flow option) |
|
||||
| `Visa` | Visa card onboarding |
|
||||
| `Twins` | Twin cards onboarding |
|
||||
| `ManageTokens` | Token management during onboarding |
|
||||
| `AskBiometry` | Biometry setup prompt |
|
||||
| `Done` | Onboarding completion |
|
||||
|
||||
### WalletRoute
|
||||
**File:** `features/wallet/impl/.../navigation/WalletRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `Wallet` | Main wallet view |
|
||||
| `OrganizeTokens` | Reorder tokens in portfolio |
|
||||
|
||||
### SendEntryRoute
|
||||
**File:** `features/send-v2/api/.../entry/SendEntryRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `Send` | Direct send flow |
|
||||
| `SendWithSwap` | Send with swap option |
|
||||
| `ChooseToken` | Token chooser for send-via-swap |
|
||||
|
||||
### CommonSendRoute (Send internal)
|
||||
Used internally by `SendModel` and `NFTSendModel`:
|
||||
- `Amount` → `Destination` → `Confirm` → `ConfirmSuccess`
|
||||
- Edit mode: `Confirm` → `Destination(edit)` or `Amount(edit)`
|
||||
|
||||
### FeeSelectorRoute (Send internal)
|
||||
- `ChooseToken` — select fee token
|
||||
- `ChooseSpeed` — select fee speed
|
||||
|
||||
### WcInnerRoute (WalletConnect)
|
||||
**File:** `features/walletconnect/impl/.../routing/WcInnerRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `Method.Send` | WC send transaction |
|
||||
| `Method.SignMessage` | WC sign message |
|
||||
| `Method.AddNetwork` | WC add network |
|
||||
| `Method.SwitchNetwork` | WC switch network |
|
||||
| `Pair` | WC pairing request |
|
||||
| `UnsupportedMethodAlert` | Unsupported method alert |
|
||||
| `WcDappDisconnected` | DApp disconnected alert |
|
||||
| `TangemUnsupportedNetwork` | Unsupported network alert |
|
||||
| `RequiredAddNetwork` | Required network add |
|
||||
| `RequiredReconnectWithNetwork` | Required network reconnect |
|
||||
|
||||
### TangemPayDetailsInnerRoute
|
||||
**File:** `features/tangempay/details/impl/.../navigation/TangemPayDetailsInnerRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `Details` | Main details view |
|
||||
| `ChangePIN` | Change PIN flow |
|
||||
| `ChangePINSuccess` | PIN change success |
|
||||
| `AddToWallet` | Add card to device wallet |
|
||||
|
||||
Transitions: `Details` → `ChangePIN` → `ChangePINSuccess`, `Details` → `AddToWallet`
|
||||
|
||||
### FeedEntryRoute
|
||||
**File:** `features/feed/api/.../components/FeedEntryRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `MarketTokenDetails` | Market token detail view |
|
||||
| `MarketTokenList` | Markets list |
|
||||
| `NewsDetail` | News article detail |
|
||||
|
||||
### CreateWalletBackupRoute
|
||||
**File:** `features/hot-wallet/impl/.../createwalletbackup/routing/CreateWalletBackupRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `RecoveryPhraseStart` | Backup intro |
|
||||
| `RecoveryPhrase` | Show recovery phrase |
|
||||
| `ConfirmBackup` | Confirm backup |
|
||||
| `BackupCompleted` | Backup complete (with upgrade/last screen flags) |
|
||||
|
||||
Transitions: `RecoveryPhraseStart` → `RecoveryPhrase` → `ConfirmBackup` → `BackupCompleted`
|
||||
|
||||
### AddExistingWalletRoute
|
||||
**File:** `features/hot-wallet/impl/.../addexistingwallet/entry/routing/AddExistingWalletRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `Import` | Seed phrase import |
|
||||
| `BackupCompleted` | Backup completed |
|
||||
| `SetAccessCode` | Set access code |
|
||||
| `ConfirmAccessCode` | Confirm access code |
|
||||
| `PushNotifications` | Push notification opt-in |
|
||||
| `SetupFinished` | Setup complete |
|
||||
|
||||
Transitions: `Import` → `BackupCompleted` → `SetAccessCode` → `ConfirmAccessCode` → `PushNotifications` → `SetupFinished`
|
||||
|
||||
### UpdateAccessCodeRoute
|
||||
**File:** `features/hot-wallet/impl/.../updateaccesscode/routing/UpdateAccessCodeRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `SetAccessCode` | Enter new access code |
|
||||
| `ConfirmAccessCode` | Confirm new access code |
|
||||
| `SetupFinished` | Update complete |
|
||||
|
||||
Transitions: `SetAccessCode` → `ConfirmAccessCode` → `SetupFinished`
|
||||
|
||||
### WalletActivationRoute
|
||||
**File:** `features/hot-wallet/impl/.../walletactivation/entry/routing/WalletActivationRoute.kt`
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `ManualBackupStart` | Backup intro |
|
||||
| `ManualBackupPhrase` | Show recovery phrase |
|
||||
| `ManualBackupCheck` | Verify backup |
|
||||
| `ManualBackupCompleted` | Backup success |
|
||||
| `SetAccessCode` | Set access code |
|
||||
| `ConfirmAccessCode` | Confirm access code |
|
||||
| `PushNotifications` | Push notification opt-in |
|
||||
| `SetupFinished` | Activation complete |
|
||||
|
||||
Transitions: `ManualBackupStart` → `ManualBackupPhrase` → `ManualBackupCheck` → `ManualBackupCompleted` → `SetAccessCode` → `ConfirmAccessCode` → `PushNotifications` → `SetupFinished`
|
||||
|
||||
## 4. Deep Links
|
||||
|
||||
### URI Schemes
|
||||
|
||||
| Scheme | Value | Usage |
|
||||
|--------|-------|-------|
|
||||
| `Tangem` | `tangem://` | Primary app deep links |
|
||||
| `WalletConnect` | `wc://` | WalletConnect pairing |
|
||||
| `Https` | `https://` | Web links (tangem.com) |
|
||||
|
||||
### Tangem Scheme Routes (`tangem://{host}`)
|
||||
|
||||
| Host | Handler | Target |
|
||||
|------|---------|--------|
|
||||
| `onramp` | `OnrampDeepLinkHandler` | Onramp callback processing |
|
||||
| `redirect_sell` | `SellRedirectDeepLinkHandler` | `Send` (sell redirect with tx params) |
|
||||
| `redirect` | — | Buy redirect (no-op) |
|
||||
| `buy` | `BuyDeepLinkHandler` | `BuyCrypto` |
|
||||
| `sell` | `SellDeepLinkHandler` | `SellCrypto` |
|
||||
| `swap` | `SwapDeepLinkHandler` | `SwapCrypto` |
|
||||
| `referral` | `ReferralDeepLinkHandler` | Referral flow |
|
||||
| `main` | `WalletDeepLinkHandler` | Wallet screen |
|
||||
| `token` | `TokenDetailsDeepLinkHandler` | `CurrencyDetails` |
|
||||
| `staking` | `StakingDeepLinkHandler` | `Staking` |
|
||||
| `markets` | `MarketsDeepLinkHandler` | `Markets` |
|
||||
| `token_chart` | `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` |
|
||||
| `wc` | `WalletConnectDeepLinkHandler` | WalletConnect pairing |
|
||||
| `promo` | `PromoDeeplinkHandler` | Promo handling |
|
||||
| `onboard-visa` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` |
|
||||
|
||||
### HTTPS Routes (`https://tangem.com/...`)
|
||||
|
||||
| Path prefix | Handler | Target |
|
||||
|-------------|---------|--------|
|
||||
| `/pay-app` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` |
|
||||
| `/news` | `NewsDetailsDeepLinkHandler` | `NewsDetails` |
|
||||
|
||||
### Deep Link Readiness
|
||||
|
||||
Deep links are only processed when the app is on a "ready" route. These routes **block** deep link processing:
|
||||
- `Initial`, `Home`, `Welcome`, `PushNotification`, `Disclaimer`, `Stories`, `Onboarding`
|
||||
19
.claude/rules/codestyle/drawable-naming.md
Normal file
19
.claude/rules/codestyle/drawable-naming.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Image Resources
|
||||
|
||||
## Naming
|
||||
|
||||
There are 3 types of icons:
|
||||
|
||||
1. Black or single color icon (naming: `ic_name_24`, where number is size)
|
||||
2. Icon with constant color, and tint could be applied (naming: `img_name_24`)
|
||||
3. Large image with different colors and shapes (naming: `ill_name`)
|
||||
|
||||
Examples:
|
||||
|
||||
1. `ic_chevron_24`
|
||||
2. `img_walletconnect_24`
|
||||
3. `ill_bussiness`
|
||||
|
||||
## Attention
|
||||
|
||||
For complex vector images (named with `ill_name`), you should use `.png` resources, because when the project is compiled, all complex vectors are converted to large, heavy PNGs for different dimensions.
|
||||
107
.claude/rules/domain/core-components.md
Normal file
107
.claude/rules/domain/core-components.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Domain Components
|
||||
|
||||
Key domain mechanisms that orchestrate data flow: suppliers, fetchers, and use cases.
|
||||
|
||||
## Retrieving Core Models
|
||||
|
||||
### UserWallet
|
||||
|
||||
#### UserWalletsListRepository
|
||||
|
||||
**Location:** `domain/common` — `com.tangem.domain.common.wallets.UserWalletsListRepository`
|
||||
|
||||
Repository for managing user wallets list. Provides `StateFlow<List<UserWallet>?>` for the wallets list and `StateFlow<UserWallet?>` for the selected wallet. Supports loading, selecting, saving, locking/unlocking (biometric, access code), deleting, and reordering wallets.
|
||||
|
||||
### Account / AccountList
|
||||
|
||||
#### SingleAccountSupplier
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.supplier.SingleAccountSupplier`
|
||||
|
||||
Supplier that provides a single `Account` by `AccountId`. Has convenience methods `filterPaymentAccount` and `filterCryptoPortfolioAccount` to filter by account subtype.
|
||||
|
||||
#### SingleAccountListSupplier
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.supplier.SingleAccountListSupplier`
|
||||
|
||||
Supplier that provides an `AccountList` for a specific user wallet by `UserWalletId`.
|
||||
|
||||
#### MultiAccountListSupplier
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.supplier.MultiAccountListSupplier`
|
||||
|
||||
Supplier that provides a list of `AccountList`s for all user wallets. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### SingleAccountListFetcher
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.fetcher.SingleAccountListFetcher`
|
||||
|
||||
Fetcher that fetches a list of accounts for a single wallet by `UserWalletId`. Extends `FlowFetcher`.
|
||||
|
||||
### AccountStatus / AccountStatusList
|
||||
|
||||
#### SingleAccountStatusSupplier
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier`
|
||||
|
||||
Supplier that provides a single `AccountStatus` by account identifier. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### SingleAccountStatusListSupplier
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier`
|
||||
|
||||
Same as `SingleAccountListSupplier` but provides `AccountStatusList` (accounts with balances) for a specific user wallet.
|
||||
|
||||
#### MultiAccountStatusListSupplier
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier`
|
||||
|
||||
Same as `MultiAccountListSupplier` but provides a list of `AccountStatusList`s for all user wallets.
|
||||
|
||||
### Network / NetworkStatus
|
||||
|
||||
#### SingleNetworkStatusSupplier
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.single.SingleNetworkStatusSupplier`
|
||||
|
||||
Supplier of `NetworkStatus` for a specific network and wallet. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### MultiNetworkStatusSupplier
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.multi.MultiNetworkStatusSupplier`
|
||||
|
||||
Supplier of all `NetworkStatus`es (as `Set<NetworkStatus>`) for a selected wallet. Extends `FlowCachingSupplier`.
|
||||
|
||||
#### SingleNetworkStatusFetcher
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.single.SingleNetworkStatusFetcher`
|
||||
|
||||
Fetcher of network status for a single `Network` by `UserWalletId`. Extends `FlowFetcher`.
|
||||
|
||||
#### MultiNetworkStatusFetcher
|
||||
|
||||
**Location:** `domain/networks` — `com.tangem.domain.networks.multi.MultiNetworkStatusFetcher`
|
||||
|
||||
Fetcher of network statuses for a set of `Network`s for a multi-currency wallet by `UserWalletId`. Extends `FlowFetcher`.
|
||||
|
||||
## Updating Balances
|
||||
|
||||
### WalletBalanceFetcher
|
||||
|
||||
**Location:** `domain/tokens` — `com.tangem.domain.tokens.wallet.WalletBalanceFetcher`
|
||||
|
||||
Fetcher of wallet balances by `UserWalletId`. Selects the appropriate fetching strategy based on wallet type (multi-wallet, single wallet with tokens, single wallet). Delegates to `BalanceFetchingOperations` for shared fetching logic.
|
||||
|
||||
### CryptoCurrencyBalanceFetcher
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher`
|
||||
|
||||
Fetches and refreshes balances for specific crypto currencies. Uses per-wallet mutexes to allow concurrent refreshes for different wallets while preventing concurrent refreshes for the same wallet. Delegates to `BalanceFetchingOperations`.
|
||||
|
||||
## Managing Portfolio (User Tokens)
|
||||
|
||||
### ManageCryptoCurrenciesUseCase
|
||||
|
||||
**Location:** `domain/account/status` — `com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase`
|
||||
|
||||
Use case for adding and removing crypto currencies in an account.
|
||||
142
.claude/rules/domain/core-models.md
Normal file
142
.claude/rules/domain/core-models.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Domain Models
|
||||
|
||||
Core business models used across the application. Models are defined in `domain/models/` and `domain/account/`.
|
||||
|
||||
## StatusSource
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.StatusSource`
|
||||
|
||||
Enum representing data loading/refresh status. Used across all status models (NetworkStatus, QuoteStatus, YieldBalance, CryptoCurrencyStatus.Sources):
|
||||
- `CACHE` — initial status, data loaded from cache
|
||||
- `ACTUAL` — terminal status, data successfully fetched from server
|
||||
- `ONLY_CACHE` — terminal status, data could not be refreshed (only cached data available)
|
||||
|
||||
## CryptoCurrency
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.currency.CryptoCurrency`
|
||||
|
||||
Sealed class representing a cryptocurrency — either a native coin (`Coin`) or a token (`Token`). Used throughout the application: portfolio, token search, swaps, buy/sell, staking, etc.
|
||||
|
||||
## CryptoCurrencyStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.currency.CryptoCurrencyStatus`
|
||||
|
||||
Model representing a currency with its balance state. Primarily used to display user's coin balance in the portfolio. Wraps `CryptoCurrency` with a `Value` sealed interface:
|
||||
|
||||
| Value subtype | Description |
|
||||
|---|---|
|
||||
| `Loading` | First-time fetch; once data is loaded, subsequent updates use cache via StatusSource, bypassing Loading |
|
||||
| `Loaded` | Full data available |
|
||||
| `Custom` | Custom token in portfolio; some data may be missing (e.g., no balance if backend has no quotes for it) |
|
||||
| `NoQuote` | Balance known, no price data |
|
||||
| `NoAccount` | Account not created (e.g., Solana reserve) |
|
||||
| `Unreachable` | Network error |
|
||||
| `NoAmount` | Coin is added to portfolio but no blockchain data available for it |
|
||||
| `MissedDerivation` | Coin has no derivations — failed to obtain a blockchain network address |
|
||||
|
||||
All Value subtypes carry `sources: Sources` tracking data freshness per dimension: `networkSource`, `quoteSource`, `stakingBalanceSource`, and aggregated `total`.
|
||||
|
||||
## Network
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.network.Network`
|
||||
|
||||
Represents a blockchain network (e.g., Ethereum, Bitcoin). Contains network metadata: ID, name, currency symbol, derivation path, standard type (ERC20, TRC20, BEP20, etc.), and capabilities (token support, transaction extras, name resolving).
|
||||
|
||||
## NetworkStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.network.NetworkStatus`
|
||||
|
||||
Blockchain balances for all tokens of a network at a specific address. Only `Verified` and `NoAccount` are cached.
|
||||
|
||||
| Value subtype | Description |
|
||||
|---|---|
|
||||
| `Verified` | Successful response from blockchain |
|
||||
| `Unreachable` | Failed response from blockchain |
|
||||
| `NoAccount` | Blockchain-specific status for chains that require a deposit to an address before it can be used |
|
||||
| `MissedDerivation` | Derivation failed — no blockchain network address |
|
||||
|
||||
## QuoteStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.quote.QuoteStatus`
|
||||
|
||||
Exchange rate between the app's selected fiat currency and a coin's currency.
|
||||
|
||||
## YieldBalance
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.staking.YieldBalance`
|
||||
|
||||
Staking yield balance for a specific `StakingID` (integrationId + address).
|
||||
|
||||
## TotalFiatBalance
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.TotalFiatBalance`
|
||||
|
||||
Aggregate fiat balance across all tokens. Sealed interface with three states: `Loading`, `Failed`, `Loaded(amount, source)`.
|
||||
|
||||
## TokenList
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.tokenlist.TokenList`
|
||||
|
||||
List of cryptocurrency tokens for display in portfolio. Sealed interface with subtypes:
|
||||
- `GroupedByNetwork` — tokens grouped by `Network`, each group contains a list of `CryptoCurrencyStatus`
|
||||
- `Ungrouped` — flat list of `CryptoCurrencyStatus`
|
||||
- `Empty` — no tokens
|
||||
|
||||
All subtypes carry `totalFiatBalance: TotalFiatBalance` and `sortedBy: TokensSortType`.
|
||||
|
||||
## Account
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.account.Account`
|
||||
|
||||
Model representing a user account. Subtypes:
|
||||
- **`Account.CryptoPortfolio`** — crypto portfolio with coins. All tokens in the account share the account's derivation (main account is an exception). Has a `DerivationIndex`: `0` for main account, `1..19` for secondary
|
||||
- **`Account.Payment`** — account for Visa card integration
|
||||
|
||||
## AccountStatus
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.account.AccountStatus`
|
||||
|
||||
Model representing an account with balances. Has a similar structure to `Account`: `CryptoPortfolio` and `Payment` subtypes.
|
||||
|
||||
## AccountList
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.models.AccountList`
|
||||
|
||||
List of all accounts for a user wallet (`UserWallet`).
|
||||
|
||||
Business rules (enforced by factory returning `Either<Error, AccountList>`):
|
||||
- Accounts list cannot be empty
|
||||
- Exactly 1 main account
|
||||
- Max 20 active accounts (`MAX_ACCOUNTS_COUNT`), max 1000 archived
|
||||
- No duplicate AccountIds or custom AccountNames
|
||||
- `totalAccounts >= activeAccounts`
|
||||
|
||||
## AccountStatusList
|
||||
|
||||
**Location:** `domain/account` — `com.tangem.domain.account.models.AccountStatusList`
|
||||
|
||||
Same as `AccountList` but with balances (wraps `AccountStatus` instead of `Account`).
|
||||
|
||||
## UserWallet
|
||||
|
||||
**Location:** `domain/models` — `com.tangem.domain.models.wallet.UserWallet`
|
||||
|
||||
Top-level model representing a user's wallet stored in the app. Subtypes:
|
||||
- **`Cold`** — wallet backed by a physical Tangem card (NFC). Contains `ScanResponse`, card info, backup state
|
||||
- **`Hot`** — software (hot) wallet without a physical card
|
||||
|
||||
## Model Hierarchy
|
||||
|
||||
```
|
||||
UserWallet
|
||||
└─ AccountList / AccountStatusList
|
||||
└─ Account.CryptoPortfolio / AccountStatus.CryptoPortfolio
|
||||
├─ CryptoCurrency (Coin | Token)
|
||||
│ └─ CryptoCurrencyStatus (currency + Value state)
|
||||
│ ├─ built from NetworkStatus (per network)
|
||||
│ ├─ built from QuoteStatus (per rawCurrencyId)
|
||||
│ └─ built from YieldBalance (per stakingId)
|
||||
├─ AccountId (SHA-256 hash)
|
||||
├─ DerivationIndex (0 = main)
|
||||
└─ CryptoPortfolioIcon (icon + color)
|
||||
```
|
||||
22
.claude/rules/git-rules.md
Normal file
22
.claude/rules/git-rules.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Git Rules
|
||||
|
||||
## Branch Naming
|
||||
|
||||
| Type | Format | Example |
|
||||
|------|--------|---------|
|
||||
| Feature | `feature/AND-xxx_short_description` | `feature/AND-13391_balance_fetcher` |
|
||||
| Bugfix | `bugfix/AND-xxx_short_description` | `bugfix/AND-14000_fix_crash` |
|
||||
| Pre-release | `x.x_pre_release` | `5.36_pre_release` |
|
||||
|
||||
**Key branches:**
|
||||
- `develop` — main integration branch, all feature/bugfix branches merge here
|
||||
- `x.x_pre_release` — branched from `develop` on the last day of sprint for the upcoming release; receives regression bugfixes and additional release items
|
||||
- `release` — merging into this branch triggers appTester build and production artifacts; PRs come from `x.x_pre_release`
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `AND-xxx Description`
|
||||
|
||||
- Start with the Jira task number (AND-xxx)
|
||||
- Followed by a space and a short description in English
|
||||
- Example: `[REDACTED_TASK_KEY] Finalize CryptoCurrencyBalanceFetcher refactoring`
|
||||
16
.claude/rules/tangem-sdk.md
Normal file
16
.claude/rules/tangem-sdk.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Tangem SDK & Libraries
|
||||
|
||||
## In-house SDKs (via `tangem_dependencies.toml`)
|
||||
|
||||
- **Blockchain SDK** (`com.tangem:blockchain`) — multichain SDK for working with blockchains: creating/signing transactions, fetching balances, managing addresses. Wrapped in `libs/blockchain-sdk/`
|
||||
- **Card SDK** (`com.tangem.tangem-sdk-kotlin:core`, `:android`) — SDK for interacting with physical Tangem cards via NFC: scanning, wallet creation, key derivation, passcode management, backup. Wrapped in `libs/tangem-sdk-api/`
|
||||
- **Hot SDK** (`com.tangem.tangem-hot-sdk-kotlin:core`, `:android`) — SDK for hot (software) wallets
|
||||
- **Vico** (`com.tangem.vico`) — forked charting library Vico, adapted for project needs
|
||||
|
||||
## Wrapper Modules (`libs/`)
|
||||
|
||||
- `libs/blockchain-sdk/` — wrapper around Blockchain SDK, provides domain-level abstractions for blockchain operations
|
||||
- `libs/tangem-sdk-api/` — wrapper around Card SDK, exposes NFC card interaction API to the app
|
||||
- `libs/crypto/` — cryptographic utilities: derivation, address handling, blockchain-specific helpers
|
||||
- `libs/auth/` — API key provider interfaces for external services (Express, StakeKit)
|
||||
- `libs/visa/` — Visa integration: smart contracts, limits, balances via Web3j
|
||||
215
.claude/skills/analyze-logs/SKILL.md
Normal file
215
.claude/skills/analyze-logs/SKILL.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
---
|
||||
name: analyze-logs
|
||||
description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation.
|
||||
allowed-tools: Read, Grep
|
||||
argument-hint: /path/to/logfile.txt [/path/to/logs.rtf]
|
||||
---
|
||||
|
||||
Analyze the Tangem app user log file at path: `$ARGUMENTS`
|
||||
|
||||
## File Input
|
||||
|
||||
The user provides one or two file paths:
|
||||
- **Log file** (`.txt`) — main application log, always required
|
||||
- **User info file** (`.rtf` or `.txt`) — optional, contains card/device/error info from the user's feedback email
|
||||
|
||||
If two paths are provided, the first is the log file and the second is the user info file.
|
||||
|
||||
**If only the log file is provided**, ask the user if they have a user info file (`logs.rtf` or `logs.txt`). If they don't have it or don't respond, fill Device Context, Card Info, and Transaction Context sections from the log file data (Steps 2+3). Mark fields that could not be determined as "N/A".
|
||||
|
||||
## User Info File (logs.rtf / logs.txt)
|
||||
|
||||
If a user info file is provided, Read it and extract the plain text fields. The file contains structured key-value pairs like:
|
||||
|
||||
```
|
||||
Card ID: AF36000002151580
|
||||
Firmware version: 6.33r
|
||||
Linked cards count: 2
|
||||
Has seed phrase: true
|
||||
Signed hashes [secp256k1]: 0
|
||||
----------
|
||||
Blockchain: Polygon
|
||||
Explorer link: https://polygonscan.com/address/0x...
|
||||
Derivation path: m/44'/60'/0'/0/0
|
||||
Host: https://rpc-mainnet.matic.quiknode.pro/
|
||||
Token: USDC
|
||||
Error: Could not construct a recoverable key.
|
||||
----------
|
||||
Source address: 0x...
|
||||
Destination address: 0x...
|
||||
Amount: 11.319684
|
||||
Fee: 0.004973
|
||||
----------
|
||||
Phone model: SM-S921B
|
||||
OS version: 36
|
||||
App version: 5.34.1
|
||||
```
|
||||
|
||||
Extract all fields and include them in the **Device Context**, **Card Info**, and **Analysis Summary** sections of the report. If the RTF contains an `Error:` field, treat it as a key clue for the investigation.
|
||||
|
||||
Note: RTF files contain formatting markup (`\cb3`, `\cf4`, `{\field{...}}`). Ignore all RTF tags — only extract the plain text values after each colon.
|
||||
|
||||
## Log Format
|
||||
|
||||
Each line follows the pattern:
|
||||
```
|
||||
DD.MM HH:MM:SS.mmm: TAG Message
|
||||
```
|
||||
|
||||
- Date format: `DD.MM` (day.month), no year — infer from context
|
||||
- Multi-line entries (JSON bodies, stack traces) continue without the timestamp prefix
|
||||
- Sensitive data is masked with `******`
|
||||
- Continuation lines may start with `|` for structured data: `|- Duration millis: 300000`
|
||||
|
||||
## Analysis Steps
|
||||
|
||||
Use `head_limit` on every Grep call to protect context from overload.
|
||||
|
||||
### Step 1: Setup
|
||||
|
||||
1. **Log time range:** Read the first and last lines with dates (format `DD.MM`)
|
||||
2. **Ask the user** (report time range, then ask):
|
||||
- Date range to focus on (or "all" for the full file)
|
||||
- Focus area: `Wallet`, `WalletConnect`, `Express (Onramp/Buy, Offramp/Sell, Swap/Exchange)`, `TangemPay`, `Feed`, `Markets`, `Settings`, `Referral`, `Staking`, `Onboarding`, `Send/Transactions`, `NFT`, or `all`
|
||||
- Remember the chosen area as **FOCUS_AREA**
|
||||
3. **Determine line range** (skip if "all"):
|
||||
- Parse input into `DD.MM` patterns (`10-13.03` → start `10.03`, end `13.03`; `last day` → last date; `last 3 days` → 3 days before last)
|
||||
- Find **START_LINE**: Grep `^START_DATE` (head_limit: 1, -n: true)
|
||||
- Find **END_LINE**: Grep `^NEXT_DATE` (head_limit: 1, -n: true). If not found, END_LINE = end of file
|
||||
- Report: "Focusing on lines START_LINE–END_LINE covering DD.MM–DD.MM"
|
||||
|
||||
### Steps 2+3+4+5+6: Main Analysis (all in parallel)
|
||||
|
||||
Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (device info may be before the date range). Steps 4+5+6 use `offset: START_LINE` to stay within the date range.
|
||||
|
||||
**Device Context (full file):**
|
||||
- `PATCH.*user-wallets/applications` (head_limit: 5, -A 10) — Read JSON body to extract `systemVersion`, `version`, `language`, `timezone`
|
||||
- `ip_address` (head_limit: 5, -A 20) — extract `alpha2`, `country`, `isBuyAllowed`, `isSellAllowed`
|
||||
|
||||
**Card Info (full file):**
|
||||
- `CardSDK_Tlv.*TAG_Firmware` (head_limit: 20)
|
||||
- `CardSDK_Tlv.*TAG_SettingsMask` (head_limit: 20)
|
||||
- `CardSDK_Tlv.*TAG_IsActivated` (head_limit: 20)
|
||||
- `CardSDK_Tlv.*TAG_ManufacturerName` (head_limit: 20)
|
||||
|
||||
**Navigation (offset: START_LINE):**
|
||||
- `AppRouter` (head_limit: 200) — if FOCUS_AREA is `all` or navigation-heavy (Wallet, Onboarding, Send/Transactions), also Read `.claude/docs/navigation-graph.md` to cross-reference routes
|
||||
|
||||
**Errors (offset: START_LINE, head_limit: 50 each, -n: true):**
|
||||
- HTTP errors: `<-- [45]\d{2}`
|
||||
- Domain errors: `DomainError`
|
||||
- App exceptions: `\bException\b`
|
||||
- Biometric errors: `onAuthenticationError`
|
||||
- Tangem Pay errors: `Failed checkCustomerWallet`
|
||||
|
||||
**Session timeline (offset: START_LINE, head_limit: 50 each):**
|
||||
- `MainActivity.*onCreate` — app session start
|
||||
- `MainActivity.*Splash screen` — splash screen installed/dismissed
|
||||
- `MainActivity.*onNewIntent` — deep link or push notification
|
||||
- `CardSDK_Session.*start card session` — NFC session starts
|
||||
|
||||
**Error filtering:** When processing error results, skip these noisy matches:
|
||||
- `java.io.IOException: Canceled` — normal request cancellation
|
||||
- `HttpException(code=304` — HTTP "Not Modified"
|
||||
- Bare stacktrace lines starting with `\tat`
|
||||
- `<-- HTTP FAILED: java.io.IOException: Canceled`
|
||||
|
||||
### Step 7: Deep Dive
|
||||
|
||||
For each significant error found above:
|
||||
1. Note the error's line number from Grep output (`-n: true`)
|
||||
2. Use Read with `offset: ERROR_LINE - 100, limit: 200` to get ~200 lines of context
|
||||
3. In that context, look for navigation events, API calls, and redux actions
|
||||
|
||||
## Key Tags Reference
|
||||
|
||||
| Tag | Purpose |
|
||||
|-----|---------|
|
||||
| `MainActivity` | Activity lifecycle, splash screen, onNewIntent |
|
||||
| `AppRouter` | Navigation: Push, Pop, Replace |
|
||||
| `NetworkLogs` | HTTP requests/responses (OkHttp) |
|
||||
| `BlockchainSDK_NETWORK` | Blockchain RPC calls |
|
||||
| `CardSDK_Tlv` | NFC card data (firmware, settings) |
|
||||
| `CardSDK_Session` | NFC session lifecycle |
|
||||
| `CardSDK_Biometric` | Biometric authentication |
|
||||
|
||||
## Common Error Patterns
|
||||
|
||||
| Pattern | Meaning |
|
||||
|---------|---------|
|
||||
| `HttpException(code=4xx/5xx, errorBody={...})` | API error with structured body |
|
||||
| `DomainError(description=...)` | App-level domain error |
|
||||
| `<-- HTTP FAILED: java.io.IOException: Canceled` | Cancelled network request (noise) |
|
||||
| `<-- 429` | Rate limiting |
|
||||
| `onAuthenticationError` | Biometric auth failure |
|
||||
|
||||
## Output Template
|
||||
|
||||
Structure your report EXACTLY as follows:
|
||||
|
||||
```
|
||||
# Log Analysis Report
|
||||
|
||||
## Device Context
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| App version | ... |
|
||||
| Android version | ... |
|
||||
| Phone model | ... (from logs.rtf if available) |
|
||||
| Language | ... |
|
||||
| Timezone | ... |
|
||||
| Country | ... |
|
||||
| Log time range | DD.MM HH:MM — DD.MM HH:MM |
|
||||
|
||||
## Card Info
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Card ID | ... (from logs.rtf if available) |
|
||||
| Firmware | ... |
|
||||
| Manufacturer | ... |
|
||||
| Is activated | ... |
|
||||
| Linked cards | ... (from logs.rtf if available) |
|
||||
| Has seed phrase | ... (from logs.rtf if available) |
|
||||
|
||||
## Transaction Context (from logs.rtf, if available)
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Blockchain | ... |
|
||||
| Token | ... |
|
||||
| Source address | ... |
|
||||
| Destination address | ... |
|
||||
| Amount | ... |
|
||||
| Fee | ... |
|
||||
| Error | ... |
|
||||
|
||||
## Navigation Path
|
||||
1. [HH:MM:SS] Screen (Push/Pop/Replace)
|
||||
2. ...
|
||||
|
||||
**Summary:** Brief description of the user's journey.
|
||||
|
||||
## Errors Found
|
||||
|
||||
### HTTP Errors
|
||||
| Time | URL | Status | Details |
|
||||
|------|-----|--------|---------|
|
||||
|
||||
### Domain Errors
|
||||
| Time | Component | Error |
|
||||
|------|-----------|-------|
|
||||
|
||||
### Other Errors
|
||||
| Time | Type | Details |
|
||||
|------|------|---------|
|
||||
|
||||
## Key Events Timeline
|
||||
| Time | Event | Details |
|
||||
|------|-------|---------|
|
||||
(chronological: app starts, card sessions, navigation, errors, notable API calls)
|
||||
|
||||
## Analysis Summary
|
||||
(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations.
|
||||
If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.)
|
||||
```
|
||||
|
||||
If a section has no data, write "None found" instead of omitting it.
|
||||
295
.claude/skills/fix-crashlytics/SKILL.md
Normal file
295
.claude/skills/fix-crashlytics/SKILL.md
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
---
|
||||
name: fix-crashlytics
|
||||
description: Auto-fix Crashlytics crashes from Jira — find [Crashlytics] tasks, analyze crash, fix code, create branches, comment on Jira. Runs on CI without prompts.
|
||||
allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent, mcp__atlassian__getAccessibleAtlassianResources, mcp__atlassian__searchJiraIssuesUsingJql, mcp__atlassian__getJiraIssue, mcp__atlassian__addCommentToJiraIssue, mcp__firebase__crashlytics_get_issue, mcp__firebase__crashlytics_list_events, mcp__firebase__firebase_get_environment
|
||||
argument-hint: [--dry-run] [--since <JQL date expression>]
|
||||
---
|
||||
|
||||
Auto-fix Crashlytics crashes reported in Jira.
|
||||
|
||||
**CRITICAL: This skill runs on CI. NEVER ask questions. If anything is ambiguous, make the safer choice or skip the task.**
|
||||
|
||||
## Constants
|
||||
|
||||
- **Jira cloudId**: `tangem.atlassian.net`
|
||||
- **Firebase project**: `tangemapp`
|
||||
- **Crashlytics appId (Release)**: `1:721920782444:android:2202a761840271413f2849`
|
||||
- **Dry-run mode**: check if `$ARGUMENTS` contains `--dry-run`. In dry-run mode, do NOT push branches and do NOT comment on Jira.
|
||||
- **Since**: check if `$ARGUMENTS` contains `--since <value>`. The value is any valid JQL date expression (e.g., `-3d`, `-1w`, `"2026-03-25"`). Default: `-1d`.
|
||||
- **Tangem SDK packages** (crashes here cannot be fixed in app code):
|
||||
- `com.tangem.blockchain` — Blockchain SDK
|
||||
- `com.tangem.sdk` — Card SDK
|
||||
- `com.tangem.hot.sdk` — Hot SDK
|
||||
- `com.tangem.vico` — Vico charting
|
||||
- `com.tangem.common.card` — Card SDK common
|
||||
- `com.tangem.common.core` — Card SDK common core
|
||||
|
||||
## Phase 0: Preflight Checks
|
||||
|
||||
Before any work, verify that all required MCP servers and tools are available. **If any check fails, STOP immediately with an error message — do not proceed.**
|
||||
|
||||
### 0a. Verify Atlassian MCP
|
||||
|
||||
Call `mcp__atlassian__getAccessibleAtlassianResources` (no parameters).
|
||||
- If the call succeeds and returns a list containing `tangem.atlassian.net` — Atlassian MCP is OK.
|
||||
- If the call fails or the tool is not found — STOP with: `FATAL: Atlassian MCP server is not connected. Run 'claude mcp list' to check server status.`
|
||||
|
||||
### 0b. Verify Firebase MCP
|
||||
|
||||
Call `mcp__firebase__firebase_get_environment` (no parameters).
|
||||
- If the call succeeds and the response contains `project_id: "tangemapp"` — Firebase MCP is OK and connected to the correct project (configured via `.firebaserc`).
|
||||
- If the project is different or missing — STOP with: `FATAL: Firebase project mismatch. Expected 'tangemapp'. Check .firebaserc configuration.`
|
||||
- If the call fails or the tool is not found — STOP with: `FATAL: Firebase MCP server is not connected. Run 'claude mcp list' to check server status.`
|
||||
|
||||
### 0c. Verify Git State
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git status --porcelain 2>&1
|
||||
```
|
||||
- If output is empty (clean working tree) — OK.
|
||||
- If there are uncommitted changes — STOP with: `FATAL: Working tree is not clean. Commit or stash changes before running this skill.`
|
||||
|
||||
### 0d. Sync with Remote
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
```
|
||||
|
||||
Initialize an internal results list to track each ticket's outcome.
|
||||
|
||||
## Phase 1: Find Crashlytics Tasks
|
||||
|
||||
Search Jira for Crashlytics tasks created in the past day:
|
||||
|
||||
- Tool: `mcp__atlassian__searchJiraIssuesUsingJql`
|
||||
- `cloudId`: `tangem.atlassian.net`
|
||||
- `jql`: `project = "AND" AND summary ~ "\\[Crashlytics\\]" AND created >= <since value> ORDER BY created DESC`
|
||||
- Use the `--since` argument value, or `-1d` if not provided.
|
||||
- `maxResults`: `50`
|
||||
- `fields`: `["summary", "status"]`
|
||||
|
||||
Collect all returned issue keys (e.g., `[REDACTED_TASK_KEY]`).
|
||||
|
||||
If no tasks found, output "No Crashlytics tasks found since <since value>" and stop.
|
||||
|
||||
## Phase 2: Filter Out Already-Branched Tasks
|
||||
|
||||
For each ticket key, check if a branch already exists:
|
||||
|
||||
```bash
|
||||
git branch -a | grep -F "<TICKET_KEY>"
|
||||
```
|
||||
|
||||
- If a branch is found: record status `Skipped (branch exists)` and remove from the processing list.
|
||||
|
||||
### 2b. Filter by Existing Comment
|
||||
|
||||
For each remaining ticket, check if it was already processed by a previous run:
|
||||
|
||||
- Call `mcp__atlassian__getJiraIssue` with `issueIdOrKey` set to the ticket key and request comments.
|
||||
- Check if any comment body starts with `**Claude Report**`.
|
||||
- If such a comment exists: record status `Skipped (already commented)` and remove from the processing list.
|
||||
|
||||
Keep only tickets that passed both filters.
|
||||
|
||||
If no tickets remain after filtering, output the summary table and stop.
|
||||
|
||||
## Phase 3: Process Each Ticket
|
||||
|
||||
Process each remaining ticket sequentially. **Error handling rule**: if ANY step fails for a ticket, record the failure reason, run `git checkout develop && git checkout -- .` to clean up, and continue to the next ticket.
|
||||
|
||||
### Step 3a: Extract Crashlytics Issue ID
|
||||
|
||||
- Call `mcp__atlassian__getJiraIssue` with `responseContentFormat: "markdown"` to get the full description.
|
||||
- Find the Crashlytics URL in the description. It looks like:
|
||||
```
|
||||
https://console.firebase.google.com/project/tangemapp/crashlytics/app/android:com.tangem.wallet/issues/<ISSUE_ID>
|
||||
```
|
||||
- Extract `<ISSUE_ID>` from the URL path (the segment after `/issues/` and before `?`).
|
||||
- If no Crashlytics link found: skip with `Skipped (no Crashlytics link)`.
|
||||
|
||||
### Step 3b: Get Crash Details from Firebase
|
||||
|
||||
- Call `mcp__firebase__crashlytics_get_issue` with:
|
||||
- `appId`: `1:721920782444:android:2202a761840271413f2849`
|
||||
- `issueId`: the extracted issue ID
|
||||
- Call `mcp__firebase__crashlytics_list_events` with:
|
||||
- `appId`: `1:721920782444:android:2202a761840271413f2849`
|
||||
- `filter`: `{"issueId": "<ISSUE_ID>"}`
|
||||
- `pageSize`: `1`
|
||||
|
||||
Extract from the response:
|
||||
- **Exception type and message** (from `subtitle` or `exceptions`)
|
||||
- **Blame frame**: file name, line number, symbol (method name)
|
||||
- **Full stacktrace** (from `exceptions` field in events)
|
||||
|
||||
Classify the crash by examining the blame frame and full stacktrace:
|
||||
|
||||
1. **App code**: blame frame is in `com.tangem.wallet` with `owner: DEVELOPER`, OR the first `com.tangem` frame in stacktrace is in app packages (`com.tangem.feature.*`, `com.tangem.core.*`, `com.tangem.data.*`, `com.tangem.domain.*`, `com.tangem.tap.*`, `com.tangem.datasource.*`). → Continue to Step 3c (fix the bug).
|
||||
|
||||
2. **Tangem SDK**: the first `com.tangem` frame in stacktrace belongs to a Tangem SDK package (see Constants). → Go to Step 3b-sdk (comment only, no fix).
|
||||
|
||||
3. **External dependency**: no `com.tangem` frames, or only third-party/Android framework code. → Go to Step 3b-ext (comment only, no fix).
|
||||
|
||||
### Step 3b-ext: Handle External Dependency Crash (comment only)
|
||||
|
||||
When the crash is in an external dependency (third-party library or Android framework), do NOT attempt to fix it. Instead, comment.
|
||||
|
||||
1. Identify the library/framework from the top frames of the stacktrace.
|
||||
|
||||
2. If NOT in `--dry-run` mode, call `mcp__atlassian__addCommentToJiraIssue`:
|
||||
- `cloudId`: `tangem.atlassian.net`
|
||||
- `issueIdOrKey`: the ticket key
|
||||
- `contentFormat`: `markdown`
|
||||
- `commentBody`:
|
||||
```
|
||||
**Claude Report**
|
||||
**Crash location:** <library/framework name> — <fully.qualified.class.method>
|
||||
**Exception:** <ExceptionType>: <message>
|
||||
**Analysis:** This crash originates in an external dependency (<library/framework name>), not in app code.
|
||||
```
|
||||
|
||||
3. Record status as `Commented (external dependency)`. Do NOT create a branch.
|
||||
|
||||
4. Continue to the next ticket.
|
||||
|
||||
### Step 3b-sdk: Handle Tangem SDK Crash (comment only)
|
||||
|
||||
When the crash is in a Tangem SDK package, do NOT attempt to fix it. Instead, analyze and comment.
|
||||
|
||||
1. Identify which SDK is affected from the package name:
|
||||
- `com.tangem.blockchain` → Blockchain SDK
|
||||
- `com.tangem.sdk` / `com.tangem.common.card` / `com.tangem.common.core` → Card SDK
|
||||
- `com.tangem.hot.sdk` → Hot SDK
|
||||
- `com.tangem.vico` → Vico
|
||||
|
||||
2. Walk the stacktrace to find the first app-code frame (caller context).
|
||||
|
||||
3. Analyze the crash: what exception, what method, what likely input caused it.
|
||||
|
||||
4. If NOT in `--dry-run` mode, call `mcp__atlassian__addCommentToJiraIssue`:
|
||||
- `cloudId`: `tangem.atlassian.net`
|
||||
- `issueIdOrKey`: the ticket key
|
||||
- `contentFormat`: `markdown`
|
||||
- `commentBody`:
|
||||
```
|
||||
**Claude Report**
|
||||
**Crash location:** <SDK name> — <fully.qualified.class.method>
|
||||
**Exception:** <ExceptionType>: <message>
|
||||
**App context:** Called from <app_class.method> at <file:line>
|
||||
**Analysis:** <what went wrong — likely cause based on stacktrace and exception message>
|
||||
**Recommendation:** This crash originates in Tangem <SDK name>. A fix requires an SDK update.
|
||||
```
|
||||
|
||||
5. Record status as `Commented (SDK — <SDK name>)`. Do NOT create a branch.
|
||||
|
||||
6. Continue to the next ticket.
|
||||
|
||||
### Step 3c: Find and Read the Crashing File
|
||||
|
||||
1. Extract the simple class name from the blame frame's `symbol` (e.g., `com.tangem.feature.foo.BarClass.method` -> `BarClass`).
|
||||
2. Use `Glob("**/<ClassName>.kt")` to find the file.
|
||||
3. If multiple files match, use the full package path from the stacktrace to disambiguate.
|
||||
4. `Read` the file. Focus on the method and line number from the blame frame.
|
||||
5. Use `Grep` to understand related types, method signatures, or null-safety context if needed.
|
||||
|
||||
If the file cannot be found: skip with `Skipped (file not found)`.
|
||||
|
||||
### Step 3d: Fix the Bug
|
||||
|
||||
Apply a **minimal, defensive fix** based on the crash type. Do NOT refactor, add features, or clean up surrounding code.
|
||||
|
||||
**Fix patterns by exception type:**
|
||||
|
||||
| Exception | Fix Strategy |
|
||||
|-----------|-------------|
|
||||
| `NullPointerException` | Add null-checks. Use `?.` safe calls, `?: return`/`?: default` for fallback. For Moshi-deserialized models where Kotlin non-null types can be JVM-null, cast to nullable: `val x = obj.field as Type?` then null-check. Use `getOrNull()` instead of `[]` for collections. |
|
||||
| `IndexOutOfBoundsException` | Add bounds checking. Use `getOrNull()`, `firstOrNull()`, `lastOrNull()`. Check `isEmpty()` before indexing. |
|
||||
| `IllegalStateException` | Check state before access. For `lateinit` crashes: add `::property.isInitialized` check or make property nullable. For Decompose/lifecycle: guard with lifecycle state check. |
|
||||
| `IllegalArgumentException` | Validate inputs. Use `coerceIn()`, `coerceAtLeast(0)`, `maxOf(0, value)`. For `BigDecimal` formatting issues: handle negative or zero values. |
|
||||
| `ClassCastException` | Use `as?` safe cast with fallback. |
|
||||
| `ConcurrentModificationException` | Copy collection before iteration: `.toList()`. |
|
||||
|
||||
**Rules:**
|
||||
- Only change the file identified in the blame frame.
|
||||
- Make the smallest possible change that prevents the crash.
|
||||
- Use `Edit` tool for precise changes (not `Write` for the whole file).
|
||||
- Follow existing code patterns in the file (logging, error handling style).
|
||||
- Do NOT add comments explaining the fix — the commit message and Jira comment handle that.
|
||||
|
||||
### Step 3e: Build Verification
|
||||
|
||||
1. Determine the Gradle module from the file path:
|
||||
- Take the path relative to the project root, up to (not including) `src/`.
|
||||
- Replace `/` with `:` and prepend `:`.
|
||||
- Example: `features/tokendetails/impl/src/...` -> `:features:tokendetails:impl`
|
||||
- Special case: `app/src/...` -> `:app` (use `assembleGoogleDebug` instead of `assembleDebug`)
|
||||
|
||||
2. Run the build:
|
||||
```bash
|
||||
./gradlew :<module>:assembleDebug
|
||||
# or for :app module:
|
||||
./gradlew :app:assembleGoogleDebug
|
||||
```
|
||||
|
||||
3. If build fails:
|
||||
- Read the error, attempt to fix it (one retry only).
|
||||
- If still fails: `git checkout -- .` and skip with `Failed (build failed)`.
|
||||
|
||||
### Step 3f: Create Branch, Commit, Push
|
||||
|
||||
```bash
|
||||
git checkout develop
|
||||
git checkout -b bugfix/<TICKET_KEY>
|
||||
git add <changed_files_only>
|
||||
git commit -m "<TICKET_KEY> Fix <ExceptionType> in <ClassName>"
|
||||
```
|
||||
|
||||
If NOT in `--dry-run` mode:
|
||||
```bash
|
||||
git push -u origin bugfix/<TICKET_KEY>
|
||||
```
|
||||
|
||||
Return to develop for the next ticket:
|
||||
```bash
|
||||
git checkout develop
|
||||
```
|
||||
|
||||
### Step 3g: Comment on Jira
|
||||
|
||||
If NOT in `--dry-run` mode, call `mcp__atlassian__addCommentToJiraIssue`:
|
||||
- `cloudId`: `tangem.atlassian.net`
|
||||
- `issueIdOrKey`: the ticket key
|
||||
- `contentFormat`: `markdown`
|
||||
- `commentBody`:
|
||||
```
|
||||
**Claude Report**
|
||||
**Root cause:** <description of what caused the crash>
|
||||
**Fix:** <description of the code change>
|
||||
**Branch:** bugfix/<TICKET_KEY>
|
||||
**Affected file:** <relative path to the changed file>
|
||||
```
|
||||
|
||||
Record status as `Fixed`.
|
||||
|
||||
## Phase 4: Output Summary
|
||||
|
||||
Output the results as a Markdown table:
|
||||
|
||||
```markdown
|
||||
## Crashlytics Auto-Fix Summary
|
||||
|
||||
| Ticket | Crash | File | Status | Branch |
|
||||
|--------|-------|------|--------|--------|
|
||||
| AND-XXXXX | NPE in ClassName.method | ClassName.kt | Fixed | bugfix/AND-XXXXX |
|
||||
| AND-YYYYY | IOOB in OtherClass.method | OtherClass.kt | Skipped (branch exists) | — |
|
||||
| AND-ZZZZZ | ISE in ThirdClass.method | ThirdClass.kt | Failed (build failed) | — |
|
||||
```
|
||||
|
||||
After the table, output totals:
|
||||
```
|
||||
**Total:** X tasks found, Y fixed, Z commented (SDK), W skipped, V failed
|
||||
```
|
||||
5
.firebaserc
Normal file
5
.firebaserc
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"projects": {
|
||||
"default": "tangemapp"
|
||||
}
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -45,3 +45,6 @@ app/src/external/google-services.json
|
|||
# Kotlin Plugin
|
||||
.kotlin/
|
||||
find-latest-release-branch.output
|
||||
|
||||
# Claude
|
||||
/.claude/worktrees/
|
||||
|
|
|
|||
14
.mcp.json
Normal file
14
.mcp.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"firebase": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "firebase-tools@latest", "mcp"]
|
||||
},
|
||||
"atlassian": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
|
||||
}
|
||||
}
|
||||
}
|
||||
114
CLAUDE.md
Normal file
114
CLAUDE.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build & Test Commands
|
||||
|
||||
```bash
|
||||
# Build debug APK (Google flavor)
|
||||
./gradlew :app:assembleGoogleDebug
|
||||
|
||||
# Run all unit tests (debug/googleDebug variants + JVM modules)
|
||||
./gradlew unitTest
|
||||
|
||||
# Run tests for a single module
|
||||
./gradlew :features:wallet:impl:testDebugUnitTest # Android library module
|
||||
./gradlew :app:testGoogleDebugUnitTest # App module
|
||||
./gradlew :domain:tokens:test # Pure JVM module
|
||||
|
||||
# Run a single test class
|
||||
./gradlew :core:ui:testDebugUnitTest --tests "com.tangem.core.ui.format.BigDecimalCryptoFormatTest"
|
||||
|
||||
# Detekt (static analysis) — runs automatically via convention plugin on applicable modules
|
||||
./gradlew detekt detektMain
|
||||
|
||||
# Build UI tests APKs (for Marathon)
|
||||
./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest
|
||||
# 1. :app:assembleGoogleMocked — main APK (mocked build)
|
||||
# 2. :app:assembleGoogleMockedAndroidTest — test APK with instrumented tests
|
||||
```
|
||||
|
||||
**Product flavors:** `google` and `huawei` (dimension: `service`). Default development flavor is `google`.
|
||||
|
||||
**Build types:** `debug`, `mocked`, `internal`, `external`, `release`.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Module Layers
|
||||
|
||||
The project is a heavily modularized Android app (~220 modules) organized in layers:
|
||||
|
||||
- **`app/`** — Application entry point, Hilt setup, navigation root
|
||||
- **`domain/`** — Business logic and models. Each domain area (e.g., `tokens`, `wallets`, `card`) has a `models` submodule for pure data types and a core module for use cases
|
||||
- **`data/`** — Repository implementations and data sources, mirrors domain structure
|
||||
- **`features/`** — UI features using **API/Impl split pattern**: `features:foo:api` defines the public contract, `features:foo:impl` contains the implementation. This enforces clean dependency boundaries
|
||||
- **`core/`** — Cross-cutting concerns: `ui`, `analytics`, `datasource`, `decompose`, `navigation`, `res`, `utils`, `security`, `pagination`
|
||||
- **`common/`** — Shared models, routing, UI components, test utilities
|
||||
- **`libs/`** — SDK wrappers: `blockchain-sdk`, `tangem-sdk-api`, `crypto`, `auth`, `visa`
|
||||
|
||||
### Component Architecture (Decompose)
|
||||
|
||||
The app uses [Decompose](https://github.com/arkivanov/Decompose) for lifecycle-aware components. Every feature screen follows this structure:
|
||||
|
||||
**API module** (`features/{name}/api/`):
|
||||
- `{Name}Component` interface implementing `ComposableContentComponent`
|
||||
- Inner `Params` data class for input parameters
|
||||
- Inner `Factory` interface: `fun create(context: AppComponentContext, params: Params): {Name}Component`
|
||||
|
||||
**Impl module** (`features/{name}/impl/`):
|
||||
- `Default{Name}Component` with `@AssistedInject` constructor taking `@Assisted appComponentContext: AppComponentContext` and `@Assisted params`
|
||||
- Delegates `AppComponentContext by appComponentContext`
|
||||
- Creates model via `getOrCreateModel(params)`
|
||||
- `@Composable Content(modifier)` collects model state via `collectAsStateWithLifecycle()`
|
||||
- Inner `@AssistedFactory` interface extending the public `Factory`
|
||||
|
||||
**Model** (`features/{name}/impl/.../model/`):
|
||||
- `{Name}Model` extending `Model` base class, annotated `@ModelScoped`, uses `@Inject` constructor
|
||||
- Receives params via `ParamsContainer.require<ParamsType>()`
|
||||
- Exposes `StateFlow<{Name}UM>` (UM = UI Model, state class in `ui/state/` subpackage)
|
||||
- Has `modelScope` (SupervisorJob + mainImmediate), auto-cancelled on destroy
|
||||
|
||||
**Child navigation within features:**
|
||||
- `childStack()` — stacked screen navigation (back stack)
|
||||
- `childSlot()` — optional overlays/bottom sheets (single or no child)
|
||||
- `InnerRouter` — feature-internal navigation that delegates unknown routes to parent router
|
||||
|
||||
### Feature Package Conventions
|
||||
|
||||
- API package: `com.tangem.features.{name}.api` (plural `features`)
|
||||
- Impl package: `com.tangem.feature.{name}.impl` (singular `feature` — legacy inconsistency, follow existing pattern per feature)
|
||||
- Component: `{Name}Component` (api), `Default{Name}Component` (impl)
|
||||
- Model: `{Name}Model` in `model/` subpackage
|
||||
- UI State: `{Name}UM` in `ui/state/` subpackage
|
||||
- UI Composable: in `ui/` subpackage
|
||||
|
||||
### Key Frameworks & Patterns
|
||||
|
||||
- **DI:** Hilt with `@SingletonComponent` scope and custom `@ModelScoped` scope for model-lifecycle dependencies
|
||||
- **UI:** Jetpack Compose with Material3. Image loading via Coil
|
||||
- **Navigation:** Custom `AppRouter` + `AppRoute` sealed classes with deep link support via `DeepLinkBuilder`
|
||||
- **Networking:** Retrofit + Moshi for API communication
|
||||
- **Local storage:** `AppPreferencesStore` for key-value pairs, `DataStore` for larger data
|
||||
- **Async:** Kotlin Coroutines + Flow. Inject `CoroutineDispatcherProvider` (from `core/utils`) instead of using `Dispatchers.*` directly — provides `main`, `mainImmediate`, `io`, `default`, `single`
|
||||
- **Error handling:** Arrow's `Either<Error, Success>` pattern throughout domain/data layers. `DataError` sealed hierarchy for domain errors. See `domain/core/CLAUDE.md` for the LCE pattern
|
||||
- **Analytics:** `AnalyticsEvent(category, event, params)` in `core/analytics/models/`. Feature events are sealed class hierarchies extending `AnalyticsEvent`. Send via injected `AnalyticsEventHandler`
|
||||
- **Feature toggles:** `FeatureTogglesManager` in `core/config-toggles/`. Toggles are defined in `core/config-toggles/src/main/assets/configs/feature_toggles_config.json` and auto-generated into a `FeatureToggles` enum by the convention plugin at build time. Each feature module exposes its own `XxxFeatureToggles` interface (in `api/`) with a `DefaultXxxFeatureToggles` implementation (in `impl/`) that delegates to `FeatureTogglesManager`
|
||||
- **Supported languages:** `SupportedLanguages` in `core/utils/` defines the app's supported locales: en, ru, de, fr, it, ja, uk, zh, es. `getCurrentSupportedLanguageCode()` returns the device locale if supported, otherwise falls back to English. Used by API calls that accept a language parameter
|
||||
|
||||
### Build System
|
||||
|
||||
- **Gradle 8.14.1**, AGP 8.10.1, Kotlin 2.1.10
|
||||
- **Version catalogs:** `gradle/dependencies.toml` (external/third-party dependencies) and `gradle/tangem_dependencies.toml` (in-house Tangem SDK dependencies)
|
||||
- **Convention plugin:** `plugins/configuration/` — applies Detekt, configures test settings, generates environment configs and feature toggles
|
||||
- **Custom Detekt rules:** `plugins/detekt-rules/`. Detekt configuration is in the `tangem-android-tools` git submodule. Key rule: `UnsafeStringResourceUsage` — prevents direct `stringResource()` / `pluralStringResource()` calls; use the `Safe`-suffixed variants instead
|
||||
- **Localization:** Managed via [Lokalise](https://lokalise.com). Update strings by running `python3 lokalize.py`
|
||||
- **GitHub Packages auth:** Requires `gpr.user` and `gpr.key` in `local.properties` for Tangem SDK dependencies
|
||||
|
||||
### Testing
|
||||
|
||||
- **JUnit 5** (Jupiter) for unit tests
|
||||
- **MockK** for mocking
|
||||
- **Turbine** for Flow testing
|
||||
- **Truth** for assertions
|
||||
- **Marathon** for UI tests (emulator-based, configured via `Marathonfile`)
|
||||
- Shared test utilities in `common:test` and `test/core/`
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ abstract class BaseTestCase : TestCase(
|
|||
"SWAP_REDESIGN_ENABLED" to false,
|
||||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||
"GASLESS_APPROVAL_ENABLED" to true,
|
||||
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import android.support.annotation.PluralsRes
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
import io.github.kakaocup.compose.node.builder.ViewBuilder
|
||||
|
||||
fun ViewBuilder.hasLazyListItemPosition(position: Int) = apply {
|
||||
addSemanticsMatcher(SemanticsMatcher.expectValue(LazyListItemPositionSemantics, position))
|
||||
}
|
||||
|
||||
fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String =
|
||||
InstrumentationRegistry.getInstrumentation()
|
||||
.targetContext
|
||||
.resources
|
||||
.getQuantityString(resId, quantity, *formatArgs)
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ fun BaseTestCase.synchronizeAddresses(
|
|||
fun BaseTestCase.openDeviceSettingsScreen() {
|
||||
step("Open wallet details") {
|
||||
waitForIdle()
|
||||
onTopBar { moreButton.clickWithAssertion() }
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.performClick() }
|
||||
|
|
@ -101,7 +101,7 @@ fun BaseTestCase.openDeviceSettingsScreen() {
|
|||
|
||||
fun BaseTestCase.openWalletConnectScreen() {
|
||||
step("Click 'More' button on TopBar") {
|
||||
onTopBar { moreButton.clickWithAssertion() }
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Wallet Connect' button") {
|
||||
onDetailsScreen { walletConnectButton.clickWithAssertion() }
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import com.tangem.screens.AlreadyUsedWalletDialogPageObject.message
|
|||
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
|
||||
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
|
||||
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
|
||||
import com.tangem.screens.ScanWarningDialogPageObject
|
||||
import com.tangem.screens.onActionIsUnavailableDialog
|
||||
import com.tangem.screens.onDataNotLoadedDialog
|
||||
import com.tangem.screens.onFailedTransactionDialog
|
||||
import com.tangem.screens.onScanWarningDialog
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
||||
fun BaseTestCase.checkFailedTransactionDialog() {
|
||||
|
|
@ -31,21 +31,21 @@ fun BaseTestCase.checkFailedTransactionDialog() {
|
|||
}
|
||||
}
|
||||
|
||||
fun checkScanWarningDialog() {
|
||||
fun BaseTestCase.checkScanWarningDialog() {
|
||||
step("Assert 'Scan warning' dialog title is displayed") {
|
||||
ScanWarningDialogPageObject { warningTitle.isDisplayed() }
|
||||
onScanWarningDialog { warningTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert warning dialog message is displayed") {
|
||||
ScanWarningDialogPageObject { warningMessage.isDisplayed() }
|
||||
onScanWarningDialog { warningMessage.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Cancel' button is displayed") {
|
||||
ScanWarningDialogPageObject { cancelButton.isDisplayed() }
|
||||
onScanWarningDialog { cancelButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'How to scan' button is displayed") {
|
||||
ScanWarningDialogPageObject { howToScanButton.isDisplayed() }
|
||||
onScanWarningDialog { howToScanButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Request support' button is displayed") {
|
||||
ScanWarningDialogPageObject { requestSupportButton.isDisplayed() }
|
||||
onScanWarningDialog { requestSupportButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.AccountDetailsScreenTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
||||
class AccountDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AccountDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topAppBarBackButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
}
|
||||
|
||||
val manageTokensButton: KNode = child {
|
||||
hasTestTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAccountDetails(function: AccountDetailsPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.screens
|
|||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.DetailsScreenTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.wallet.R
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
|
|
@ -10,10 +11,11 @@ import io.github.kakaocup.compose.node.element.KNode
|
|||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DetailsPageObject>(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(DetailsScreenTestTags.SCREEN_CONTAINER) }
|
||||
) {
|
||||
ComposeScreen<DetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topAppBarBackButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
}
|
||||
|
||||
val walletConnectButton: KNode = child {
|
||||
hasTestTag(DetailsScreenTestTags.SCREEN_ITEM)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.test.SemanticsMatcher
|
|||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.getQuantityString
|
||||
import com.tangem.common.extensions.hasLazyListItemPosition
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.test.*
|
||||
|
|
@ -154,21 +155,27 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val seedPhraseNotificationIcon: KNode = child {
|
||||
hasAnySibling(withText(getResourceString(R.string.warning_seedphrase_issue_title)))
|
||||
val missingAddressNotificationIcon: KNode = child {
|
||||
hasAnySibling(withText(getResourceString(R.string.warning_missing_derivation_title)))
|
||||
hasTestTag(NotificationTestTags.ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val seedPhraseNotificationTitle: KNode = child {
|
||||
val missingAddressNotificationTitle: KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.warning_seedphrase_issue_title))
|
||||
hasText(getResourceString(R.string.warning_missing_derivation_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val seedPhraseNotificationMessage: KNode = child {
|
||||
fun missingAddressNotificationMessage(networkCount: Int): KNode = child {
|
||||
hasTestTag(NotificationTestTags.MESSAGE)
|
||||
hasText(getResourceString(R.string.warning_seedphrase_issue_message))
|
||||
hasText(
|
||||
getQuantityString(
|
||||
R.plurals.warning_missing_derivation_message,
|
||||
networkCount,
|
||||
networkCount
|
||||
)
|
||||
)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
@ -207,6 +214,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied))
|
||||
}
|
||||
|
||||
val organizeTokensButtonNode: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Find token list item with title and address
|
||||
*/
|
||||
|
|
@ -221,6 +233,17 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||
hasText(tokenTitle)
|
||||
}.child<KNode> {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun organizeTokensButton(): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,18 @@ import io.github.kakaocup.compose.node.element.ComposeScreen
|
|||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
|
||||
class TopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<TopBarPageObject>(
|
||||
class MainScreenTopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<MainScreenTopBarPageObject>(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(MainScreenTestTags.TOP_BAR) }
|
||||
) {
|
||||
|
||||
val moreButton: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.MORE_BUTTON)
|
||||
hasPosition(1)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onTopBar(function: TopBarPageObject.() -> Unit) =
|
||||
internal fun BaseTestCase.onMainScreenTopBar(function: MainScreenTopBarPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.R
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||
import com.tangem.core.ui.test.ManageTokensScreenTestTags
|
||||
import com.tangem.core.ui.test.SwitchTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
import androidx.compose.ui.test.hasAnySibling as withAnySibling
|
||||
import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant
|
||||
import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor
|
||||
|
||||
class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ManageTokensPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val searchField: KNode = child {
|
||||
hasTestTag(BaseSearchBarTestTags.SEARCH_BAR)
|
||||
}
|
||||
|
||||
fun tokenItem(tokenName: String): KNode = child {
|
||||
hasTestTag(ManageTokensScreenTestTags.TOKEN_ITEM)
|
||||
hasText(tokenName)
|
||||
}
|
||||
|
||||
fun networkSwitch(networkName: String): KNode = child {
|
||||
useUnmergedTree = true
|
||||
addSemanticsMatcher(
|
||||
withTestTag(SwitchTestTags.SWITCH)
|
||||
.and(
|
||||
withAnyAncestor(
|
||||
withAnySibling(
|
||||
withTestTag(ManageTokensScreenTestTags.NETWORK_NAME)
|
||||
.and(withAnyDescendant(withText(networkName)))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val saveButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_save))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onManageTokensScreen(function: ManageTokensPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -1,36 +1,42 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import com.kaspersky.kaspresso.screens.KScreen
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.tap.features.scanfails.ui.ScanFailsDialogTestTags
|
||||
import com.tangem.wallet.R
|
||||
import io.github.kakaocup.kakao.text.KTextView
|
||||
import io.github.kakaocup.kakao.text.KButton
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
object ScanWarningDialogPageObject : KScreen<ScanWarningDialogPageObject>() {
|
||||
class ScanWarningDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ScanWarningDialogPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
override val layoutId: Int? = null
|
||||
override val viewClass: Class<*>? = null
|
||||
|
||||
val warningTitle = KTextView {
|
||||
withText(R.string.common_warning)
|
||||
val warningTitle: KNode = child {
|
||||
hasText(getResourceString(R.string.common_warning))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val warningMessage = KTextView {
|
||||
withText(R.string.alert_troubleshooting_scan_card_title)
|
||||
val warningMessage: KNode = child {
|
||||
hasText(getResourceString(R.string.alert_troubleshooting_scan_card_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val tryAgainButton = KButton {
|
||||
withId(R.id.try_again_button)
|
||||
val howToScanButton: KNode = child {
|
||||
hasTestTag(ScanFailsDialogTestTags.HOW_TO_SCAN_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val howToScanButton = KButton {
|
||||
withId(R.id.how_to_scan_button)
|
||||
val requestSupportButton: KNode = child {
|
||||
hasTestTag(ScanFailsDialogTestTags.REQUEST_SUPPORT_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val requestSupportButton = KButton {
|
||||
withId(R.id.request_support_button)
|
||||
}
|
||||
|
||||
val cancelButton = KButton {
|
||||
withId(R.id.cancel_button)
|
||||
val cancelButton: KNode = child {
|
||||
hasTestTag(ScanFailsDialogTestTags.CANCEL_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onScanWarningDialog(function: ScanWarningDialogPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -165,10 +165,6 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT)
|
||||
}
|
||||
|
||||
val receiveFiatAmountWithPriceImpactWarning: KNode = child {
|
||||
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING)
|
||||
}
|
||||
|
||||
val receiveFiatAmountInformationIcon: KNode = child {
|
||||
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON)
|
||||
useUnmergedTree = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.test.BaseDialogTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
class UnableToHideDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<UnableToHideDialogPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
fun unableToHideTokenTitle(tokenName: String): KNode = child {
|
||||
hasTestTag(BaseDialogTestTags.TITLE)
|
||||
hasText(getResourceString(
|
||||
R.string.token_details_unable_hide_alert_title,
|
||||
tokenName))
|
||||
}
|
||||
|
||||
fun unableToHideTokenMessage(tokenName: String, tokenSymbol: String, networkName: String): KNode = child {
|
||||
hasTestTag(BaseDialogTestTags.TEXT)
|
||||
hasText(
|
||||
getResourceString(
|
||||
R.string.token_details_unable_hide_alert_message,
|
||||
tokenName,
|
||||
tokenSymbol,
|
||||
networkName
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val okButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_ok))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onUnableToHideDialog(function: UnableToHideDialogPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -2,18 +2,22 @@ package com.tangem.screens
|
|||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.core.ui.test.WalletSettingsScreenTestTags
|
||||
import com.tangem.wallet.R
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<WalletSettingsPageObject>(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(WalletSettingsScreenTestTags.SCREEN_CONTAINER) }
|
||||
) {
|
||||
ComposeScreen<WalletSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topAppBarBackButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
}
|
||||
|
||||
private val walletSettingsItem: KNode = child {
|
||||
hasTestTag(WalletSettingsScreenTestTags.SCREEN_ITEM)
|
||||
}
|
||||
|
|
@ -21,15 +25,24 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi
|
|||
val linkMoreCardsButton: KNode = walletSettingsItem.child {
|
||||
hasText(getResourceString(R.string.details_row_title_create_backup))
|
||||
}
|
||||
|
||||
val deviceSettingsButton: KNode = walletSettingsItem.child {
|
||||
hasText(getResourceString(R.string.card_settings_title))
|
||||
}
|
||||
|
||||
val referralProgramButton: KNode = walletSettingsItem.child {
|
||||
hasText(getResourceString(R.string.details_referral_title))
|
||||
}
|
||||
|
||||
val forgetWalletButton: KNode = walletSettingsItem.child {
|
||||
hasText(getResourceString(R.string.settings_forget_wallet))
|
||||
}
|
||||
|
||||
fun accountItem(accountName: String): KNode = walletSettingsItem.child {
|
||||
hasTestTag(WalletSettingsScreenTestTags.USER_ACCOUNT_ITEM)
|
||||
hasAnyDescendant(withText(accountName))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onWalletSettingsScreen(function: WalletSettingsPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class DetailsTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
onTopBar {
|
||||
onMainScreenTopBar {
|
||||
step("Open wallet details") {
|
||||
moreButton.clickWithAssertion()
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ class DetailsTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Wallet2)
|
||||
}
|
||||
onTopBar {
|
||||
onMainScreenTopBar {
|
||||
step("Open wallet details") {
|
||||
moreButton.clickWithAssertion()
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ class DetailsTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen(ProductType.Note)
|
||||
}
|
||||
onTopBar {
|
||||
onMainScreenTopBar {
|
||||
step("Open wallet details") {
|
||||
moreButton.clickWithAssertion()
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ class DetailsTest : BaseTestCase() {
|
|||
openMainScreen()
|
||||
}
|
||||
step("Open wallet details") {
|
||||
onTopBar { moreButton.clickWithAssertion() }
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.clickWithAssertion() }
|
||||
|
|
|
|||
|
|
@ -7,13 +7,26 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.clickAndWaitFor
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||
import com.tangem.scenarios.checkScanWarningDialog
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.screens.ThirdPartyAppPageObject
|
||||
import com.tangem.screens.onCreateWalletStartScreen
|
||||
import com.tangem.screens.onDetailsScreen
|
||||
import com.tangem.screens.onDisclaimerScreen
|
||||
import com.tangem.screens.onFailedTransactionDialog
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onScanWarningDialog
|
||||
import com.tangem.screens.onSendAddressScreen
|
||||
import com.tangem.screens.onSendConfirmScreen
|
||||
import com.tangem.screens.onSendScreen
|
||||
import com.tangem.screens.onStoriesScreen
|
||||
import com.tangem.screens.onTokenDetailsScreen
|
||||
import com.tangem.screens.onMainScreenTopBar
|
||||
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
|
|
@ -44,7 +57,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
step("Click 'More' button on TopBar") {
|
||||
waitForIdle()
|
||||
onTopBar { moreButton.clickWithAssertion() }
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click 'Contact support' button") {
|
||||
waitForIdle()
|
||||
|
|
@ -164,8 +177,10 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
step("Force show 'Scan warning' dialog"){
|
||||
runOnUiThread {
|
||||
val scanFailsState = StateDialog.ScanFailsDialog(source = StateDialog.ScanFailsSource.MAIN)
|
||||
store.dispatch(GlobalAction.ShowDialog(scanFailsState))
|
||||
val requester = store.state.daggerGraphState.scanFailsRequester!!
|
||||
MainScope().launch {
|
||||
requester.show(AnalyticsParam.ScreensSources.Main)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Check 'Scan warning' dialog") {
|
||||
|
|
@ -173,7 +188,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
checkScanWarningDialog()
|
||||
}
|
||||
step("Click on 'Request support' button") {
|
||||
ScanWarningDialogPageObject { requestSupportButton.click() }
|
||||
onScanWarningDialog { requestSupportButton.performClick() }
|
||||
}
|
||||
step("Assert 'Gmail' app is open") {
|
||||
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tests
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class MainScreenTest : BaseTestCase() {
|
||||
|
||||
@Test
|
||||
fun goToMain() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import com.tangem.tap.domain.sdk.mocks.content.*
|
|||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
|
|
@ -86,7 +85,6 @@ class ScanCardTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("870")
|
||||
@DisplayName("Scan: Card with Ed25519 curve")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -55,43 +55,4 @@ class WarningTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("227")
|
||||
@DisplayName("Seed notify: check warning for wallet with seed phrase")
|
||||
@Test
|
||||
fun checkWarningForWalletWithSeedPhraseTest() {
|
||||
val scenarioName = "seedphrase_notification"
|
||||
val scenarioState = "Notified"
|
||||
setupHooks(
|
||||
additionalBeforeSection = {
|
||||
step("Setup WireMock scenario '$scenarioName' for '$scenarioState' state") {
|
||||
setWireMockScenarioState(scenarioName, scenarioState)
|
||||
}
|
||||
},
|
||||
additionalAfterSection = {
|
||||
step("Reset WireMock scenario '$scenarioName' state") {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
}
|
||||
}
|
||||
).run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent)
|
||||
}
|
||||
step("Assert 'Seed phrase' notification icon is displayed") {
|
||||
onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Seed phrase' notification title is displayed") {
|
||||
onMainScreen { seedPhraseNotificationTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Seed phrase' notification message is displayed") {
|
||||
onMainScreen { seedPhraseNotificationMessage.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert notification 'Yes' button is displayed") {
|
||||
onMainScreen { notificationYesButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert notification 'No' button is displayed") {
|
||||
onMainScreen { notificationNoButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,23 +600,23 @@ class MainScreenActionButtonsTest : BaseTestCase() {
|
|||
step("Reset Wiremock scenario: '$scenarioName'") {
|
||||
resetWireMockScenarioState(scenarioName)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh(steps = 10)
|
||||
step("Pull to refresh and wait for buttons to become enabled") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG, intervalMs = 2_000) {
|
||||
pullToRefresh(10)
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert action buttons is enabled") {
|
||||
assertActionButtonsForMultiCurrencyWallet(isEnabled = true)
|
||||
}
|
||||
}
|
||||
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
|
||||
}
|
||||
step("Perform pull to refresh") {
|
||||
pullToRefresh(steps = 10)
|
||||
step("Pull to refresh and wait for buttons to become disabled") {
|
||||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG, intervalMs = 2_000) {
|
||||
pullToRefresh(10)
|
||||
waitForIdle()
|
||||
}
|
||||
step("Assert action buttons is not enabled") {
|
||||
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import androidx.compose.ui.test.longClick
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.*
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class HideTokenTest : BaseTestCase() {
|
||||
|
||||
@AllureId("3638")
|
||||
@DisplayName("Main: hide token by long tap")
|
||||
@Test
|
||||
fun hideTokenByLongTapTest() {
|
||||
val tokenTitle = "Polygon"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Long click on token with name: '$tokenTitle'") {
|
||||
waitForIdle()
|
||||
onMainScreen {
|
||||
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
|
||||
longClick(
|
||||
position = center,
|
||||
durationMillis = 1000L
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'Hide token' button") {
|
||||
onTokenActionsBottomSheet { hideTokenButton.performClick() }
|
||||
}
|
||||
step("Click 'Hide' button in dialog") {
|
||||
onDialog {
|
||||
dialogContainer.assertIsDisplayed()
|
||||
okButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is not displayed") {
|
||||
onMainScreen { assertTokenDoesNotExist(tokenTitle) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3627")
|
||||
@DisplayName("Main: hide token via Manage tokens")
|
||||
@Test
|
||||
fun hideTokenViaManageTokensTest() {
|
||||
val tokenTitle = "Tether"
|
||||
val networkTitle = "ETHEREUM"
|
||||
val scenarioState = "USDT"
|
||||
val accountName = "Main account"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open wallet details") {
|
||||
waitForIdle()
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.performClick() }
|
||||
}
|
||||
step("Click on account: '$accountName'") {
|
||||
onWalletSettingsScreen { accountItem(accountName).performClick() }
|
||||
}
|
||||
step("Click on 'Manage tokens' button") {
|
||||
onAccountDetails { manageTokensButton.performClick() }
|
||||
}
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Assert switch is on") {
|
||||
onManageTokensScreen { networkSwitch(networkTitle).assertIsOn() }
|
||||
}
|
||||
step("Click on '$networkTitle' switch") {
|
||||
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
|
||||
}
|
||||
step("Click 'Hide' button in dialog") {
|
||||
onDialog {
|
||||
dialogContainer.assertIsDisplayed()
|
||||
hideButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Assert switch is off") {
|
||||
onManageTokensScreen { networkSwitch(networkTitle).assertIsOff() }
|
||||
}
|
||||
step("Click on 'Save' button") {
|
||||
onManageTokensScreen { saveButton.performClick() }
|
||||
}
|
||||
step("Click on 'Account details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onAccountDetails { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Wallet settings' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onWalletSettingsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onDetailsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is not displayed") {
|
||||
onMainScreen { assertTokenDoesNotExist(tokenTitle) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3626")
|
||||
@DisplayName("Main: hide main coin via manage tokens")
|
||||
@Test
|
||||
fun hideMainCoinViaManageTokensTest() {
|
||||
val tokenTitle = "POL (ex-MATIC)"
|
||||
val networkTitle = "POLYGON"
|
||||
val polygonTitle = "Polygon"
|
||||
val accountName = "Main account"
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Open wallet details") {
|
||||
waitForIdle()
|
||||
onMainScreenTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open 'Wallet settings' screen") {
|
||||
onDetailsScreen { walletNameButton.performClick() }
|
||||
}
|
||||
step("Click on account: '$accountName'") {
|
||||
onWalletSettingsScreen { accountItem(accountName).performClick() }
|
||||
}
|
||||
step("Click on 'Manage tokens' button") {
|
||||
onAccountDetails { manageTokensButton.performClick() }
|
||||
}
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
onManageTokensScreen { tokenItem(tokenTitle).performClick() }
|
||||
}
|
||||
step("Assert switch is on") {
|
||||
onManageTokensScreen { networkSwitch(networkTitle).assertIsOn() }
|
||||
}
|
||||
step("Click on '$networkTitle' switch") {
|
||||
onManageTokensScreen { networkSwitch(networkTitle).performClick() }
|
||||
}
|
||||
step("Click 'Hide' button in dialog") {
|
||||
onDialog {
|
||||
dialogContainer.assertIsDisplayed()
|
||||
hideButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Assert switch is off") {
|
||||
onManageTokensScreen { networkSwitch(networkTitle).assertIsOff() }
|
||||
}
|
||||
step("Click on 'Save' button") {
|
||||
onManageTokensScreen { saveButton.performClick() }
|
||||
}
|
||||
step("Click on 'Account details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onAccountDetails { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Wallet settings' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onWalletSettingsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Click on 'Details' screen 'Back' button") {
|
||||
waitForIdle()
|
||||
onDetailsScreen { topAppBarBackButton.performClick() }
|
||||
}
|
||||
step("Assert token: '$polygonTitle' is not displayed") {
|
||||
onMainScreen { assertTokenDoesNotExist(polygonTitle) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3610")
|
||||
@DisplayName("Main: check 'Unable to hide token' warning")
|
||||
@Test
|
||||
fun checkUnableToHideTokenWarningTest() {
|
||||
val tokenTitle = "Ethereum"
|
||||
val tokenSymbol = "ETH"
|
||||
val scenarioState = "USDT"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Long click on token with name: '$tokenTitle'") {
|
||||
waitForIdle()
|
||||
onMainScreen {
|
||||
tokenWithTitleAndAddress(tokenTitle).performTouchInput {
|
||||
longClick(
|
||||
position = center,
|
||||
durationMillis = 1000L
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'Hide token' button") {
|
||||
onTokenActionsBottomSheet { hideTokenButton.performClick() }
|
||||
}
|
||||
step("Assert 'Unable to hide $tokenTitle' alert title is displayed") {
|
||||
onUnableToHideDialog {
|
||||
unableToHideTokenTitle(tokenName = tokenTitle).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert 'Unable to hide $tokenTitle' alert message is displayed") {
|
||||
onUnableToHideDialog {
|
||||
unableToHideTokenMessage(
|
||||
tokenName = tokenTitle,
|
||||
tokenSymbol = tokenSymbol,
|
||||
networkName = tokenTitle
|
||||
).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Ok' button") {
|
||||
onUnableToHideDialog {
|
||||
okButton.performClick()
|
||||
}
|
||||
}
|
||||
step("Press 'Back' button") {
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on token: '$tokenTitle'") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||
}
|
||||
step("Assert 'Token details screen' open") {
|
||||
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
|
||||
}
|
||||
step("Click 'More' button") {
|
||||
onTokenDetailsTopBar { moreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click 'Hide token' button") {
|
||||
onPopUpMenu {
|
||||
popUpContainer.assertIsDisplayed()
|
||||
hideTokenButton.clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Assert 'Unable to hide $tokenSymbol' alert title is displayed") {
|
||||
onUnableToHideDialog {
|
||||
unableToHideTokenTitle(tokenName = tokenSymbol).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert 'Unable to hide $tokenTitle' alert message is displayed") {
|
||||
onUnableToHideDialog {
|
||||
unableToHideTokenMessage(
|
||||
tokenName = tokenTitle,
|
||||
tokenSymbol = tokenSymbol,
|
||||
networkName = tokenTitle
|
||||
).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on 'Ok' button") {
|
||||
onUnableToHideDialog {
|
||||
okButton.performClick()
|
||||
}
|
||||
}
|
||||
step("Click 'Back' button") {
|
||||
onTokenDetailsTopBar { backButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert token: '$tokenTitle' is displayed") {
|
||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class MainScreenTest : BaseTestCase() {
|
||||
|
||||
@AllureId("66")
|
||||
@DisplayName("Main: check 'Organize tokens' button with multiple tokens no accounts")
|
||||
@Test
|
||||
fun checkOrganizeTokensButtonWithMultipleTokensNoAccountsTest() {
|
||||
|
||||
setupHooks().run {
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is displayed") {
|
||||
onMainScreen { organizeTokensButton().assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8748")
|
||||
@DisplayName("Main: check 'Organize tokens' button with single token no accounts")
|
||||
@Test
|
||||
fun checkOrganizeTokensButtonWithSingleTokenNoAccountsTest() {
|
||||
val scenarioState = "Cardano"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8749")
|
||||
@DisplayName("Main: check 'Organize tokens' button with single token two accounts")
|
||||
@Test
|
||||
fun checkOrganizeTokensButtonWithSingleTokenMultiAccountsTest() {
|
||||
val scenarioState = "TwoAccountsSingleTokenEach"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("8750")
|
||||
@DisplayName("Main: check 'Organize tokens' button with multiple tokens two accounts")
|
||||
@Test
|
||||
fun checkOrganizeTokensButtonWithMultipleTokensMultiAccountsTest() {
|
||||
val scenarioState = "TwoAccountsMixed"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonNode.assertIsDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class TokenListTest : BaseTestCase() {
|
||||
|
||||
@AllureId("180")
|
||||
@DisplayName("Token list: hide token by long tap")
|
||||
@Test
|
||||
fun checkCustomDerivationIconOnTokenAndNetworkTest() {
|
||||
val networkTitle = "Ethereum"
|
||||
val customTokenTitle = "Myria"
|
||||
val scenarioState = "CustomDerivation"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert token: '$networkTitle' is displayed") {
|
||||
onMainScreen { tokenWithTitleAndAddress(networkTitle).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert token with custom derivation icon: '$customTokenTitle' is displayed") {
|
||||
onMainScreen { tokenWithCustomDerivationIcon(customTokenTitle).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.tests.main
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||
import com.tangem.common.utils.resetWireMockScenarioState
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class WarningsTest : BaseTestCase() {
|
||||
|
||||
@AllureId("184")
|
||||
@DisplayName("Token list: hide token by long tap")
|
||||
@Test
|
||||
fun checkUnavailableNetworksWarningTest() {
|
||||
val scenarioState = "MissingDerivation"
|
||||
val networkCount = 1
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||
}
|
||||
).run {
|
||||
|
||||
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
|
||||
setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState)
|
||||
}
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Synchronize addresses") {
|
||||
synchronizeAddresses(isBalanceAvailable = false)
|
||||
}
|
||||
step("Assert 'Missing addresses' notification icon is displayed") {
|
||||
onMainScreen { missingAddressNotificationIcon.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Missing addresses' notification title is displayed") {
|
||||
onMainScreen { missingAddressNotificationTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Missing addresses' notification message is displayed") {
|
||||
onMainScreen { missingAddressNotificationMessage(networkCount).assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class SendAmountScreenTest : BaseTestCase() {
|
|||
val manualSendAmount = "1"
|
||||
val clipboardSendAmount = "0.5"
|
||||
val invalidAmount = "2"
|
||||
val errorText = getResourceString(R.string.send_validation_amount_exceeds_balance)
|
||||
val errorText = getResourceString(R.string.common_insufficient_balance)
|
||||
val context = device.context
|
||||
|
||||
setupHooks().run {
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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,11 +95,14 @@ 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") {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ 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
|
||||
|
|
@ -33,7 +32,6 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount)
|
||||
private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text)
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4287")
|
||||
@DisplayName("Warnings: check warning, when sending less than reserve")
|
||||
@Test
|
||||
|
|
@ -87,7 +85,6 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4286")
|
||||
@DisplayName("Warnings: check warning when sending amount equal to reserve")
|
||||
@Test
|
||||
|
|
@ -142,7 +139,6 @@ class StellarWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Ignore("TODO: [REDACTED_JIRA]")
|
||||
@AllureId("4288")
|
||||
@DisplayName("Warnings: check warning when sending greater than reserve")
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
|||
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
|
||||
onSwapTokenScreen {
|
||||
waitForIdle()
|
||||
receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true)
|
||||
receiveFiatAmount.assertTextContains("%", substring = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,7 +288,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
step("Assert fiat amount with warning is displayed") {
|
||||
onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) }
|
||||
onSwapTokenScreen { receiveFiatAmount.assertTextContains("%", substring = true) }
|
||||
}
|
||||
step("Assert receive amount information icon is displayed") {
|
||||
onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 009cf6332a72cf0893167221abf7010d033906c2
|
||||
Subproject commit 4bfa9f04b8c4e81e31f9a924aeb45a1b024e31ce
|
||||
|
|
@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
|||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.ScanFailsRequester
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
|
|
@ -151,4 +152,6 @@ interface ApplicationEntryPoint {
|
|||
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
|
||||
|
||||
fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles
|
||||
|
||||
fun getScanFailsRequester(): ScanFailsRequester
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import com.tangem.operations.backup.BackupService
|
|||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.common.ActivityResultCallbackHolder
|
||||
import com.tangem.tap.common.DialogManager
|
||||
import com.tangem.tap.common.OnActivityResultCallback
|
||||
import com.tangem.tap.common.analytics.events.Push
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
|
|
@ -67,11 +66,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
|
||||
|
|
@ -171,16 +170,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||
|
||||
private val dialogManager = DialogManager()
|
||||
|
||||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Timber.i("onCreate")
|
||||
TangemLogger.i("onCreate")
|
||||
// We need to call it before onCreate to prevent unnecessary activity recreation
|
||||
installAppTheme()
|
||||
|
||||
val splashScreen = installSplashScreen()
|
||||
TangemLogger.i("Splash screen installed")
|
||||
|
||||
enableEdgeToEdge(
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
|
|
@ -322,18 +320,16 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Timber.i("onStart")
|
||||
dialogManager.onStart(this)
|
||||
TangemLogger.i("onStart")
|
||||
}
|
||||
|
||||
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 +362,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 +429,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") }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@ import com.tangem.tap.common.redux.appReducer
|
|||
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import dagger.hilt.EntryPoints
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
|
||||
lateinit var store: Store<AppState>
|
||||
|
||||
|
|
@ -239,6 +239,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
private val customerIoFeatureToggles: CustomerIoFeatureToggles
|
||||
get() = entryPoint.getCustomerIoFeatureToggles()
|
||||
|
||||
private val scanFailsRequester
|
||||
get() = entryPoint.getScanFailsRequester()
|
||||
|
||||
// endregion
|
||||
|
||||
private val appScope = MainScope()
|
||||
|
|
@ -270,22 +273,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 +286,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)
|
||||
|
|
@ -384,11 +371,28 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
|||
userWalletsListRepository = userWalletsListRepository,
|
||||
tangemHotSdk = tangemHotSdk,
|
||||
trackingContextProxy = trackingContextProxy,
|
||||
scanFailsRequester = scanFailsRequester,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.ui.ScanFailsDialog
|
||||
import com.tangem.tap.common.ui.SimpleAlertDialog
|
||||
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
|
||||
import com.tangem.tap.common.ui.SimpleOkDialog
|
||||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class DialogManager : StoreSubscriber<GlobalState> {
|
||||
var context: Context? = null
|
||||
private var dialog: Dialog? = null
|
||||
|
||||
fun onStart(context: Context) {
|
||||
this.context = context
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.globalState == newState.globalState
|
||||
}.select { it.globalState }
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
this.context = null
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod")
|
||||
override fun newState(state: GlobalState) {
|
||||
if (state.dialog == null) {
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
return
|
||||
}
|
||||
val context = context ?: return
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
private val backgroundHandler = Handler(HandlerThread("AppMainHandlerThread").apply { start() }.looper)
|
||||
|
||||
fun postUi(ms: Long = 0, func: Runnable) {
|
||||
if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms)
|
||||
}
|
||||
|
||||
fun postUiDelayBg(ms: Long, func: Runnable) {
|
||||
backgroundHandler.postDelayed({ uiHandler.post(func) }, ms)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.tap.common.analytics
|
||||
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.core.analytics.api.ExceptionLogger
|
||||
import com.tangem.core.analytics.api.EventLogger
|
||||
import timber.log.Timber
|
||||
import com.tangem.core.analytics.api.ExceptionLogger
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
class AnalyticsEventsLogger(
|
||||
private val name: String,
|
||||
|
|
@ -11,11 +11,11 @@ class AnalyticsEventsLogger(
|
|||
) : EventLogger, ExceptionLogger {
|
||||
|
||||
override fun logEvent(event: String, params: Map<String, String>) {
|
||||
Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
|
||||
TangemLogger.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
|
||||
}
|
||||
|
||||
override fun logException(error: Throwable, params: Map<String, String>) {
|
||||
Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params)))
|
||||
TangemLogger.e(jsonConverter.prettyPrint(PrintEventModel(name, "error", params)), error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.common.analytics.appsflyer
|
||||
|
||||
import com.appsflyer.AppsFlyerConversionListener
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ class TangemAFConversionListener @Inject constructor(
|
|||
) : AppsFlyerConversionListener {
|
||||
|
||||
override fun onConversionDataSuccess(p0: Map<String?, Any?>?) {
|
||||
Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
|
||||
TangemLogger.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
|
||||
|
||||
if (p0 == null) return
|
||||
|
||||
|
|
@ -19,14 +19,14 @@ class TangemAFConversionListener @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onConversionDataFail(p0: String?) {
|
||||
Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
|
||||
TangemLogger.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
|
||||
}
|
||||
|
||||
override fun onAppOpenAttribution(p0: Map<String?, String?>?) {
|
||||
Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
|
||||
TangemLogger.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
|
||||
}
|
||||
|
||||
override fun onAttributionFailure(p0: String?) {
|
||||
Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
|
||||
TangemLogger.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSo
|
|||
),
|
||||
) {
|
||||
enum class Buttons(val event: String) {
|
||||
TRY_AGAIN("Try again button"),
|
||||
HOW_TO_SCAN("Button blog"),
|
||||
TRY_AGAIN("Button try again"),
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,16 @@ import com.appsflyer.attribution.AppsFlyerRequestListener
|
|||
import com.tangem.core.analytics.api.EventLogger
|
||||
import com.tangem.core.analytics.api.UserIdHolder
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener
|
||||
import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener
|
||||
import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder
|
||||
|
||||
|
|
@ -40,9 +40,9 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
|
||||
init(apiKey, tangemAFConversionListener, context)
|
||||
|
||||
Timber.i("Starting AppsFlyer SDK")
|
||||
TangemLogger.i("Starting AppsFlyer SDK")
|
||||
start(context, apiKey, InitializationListener)
|
||||
Timber.i("AppsFlyer SDK started")
|
||||
TangemLogger.i("AppsFlyer SDK started")
|
||||
|
||||
saveUID()
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun logEvent(event: String, params: Map<String, String>) {
|
||||
Timber.tag("AppsFlyer").i("Logging event: $event with params: $params")
|
||||
TangemLogger.withTag("AppsFlyer").i("Logging event: $event with params: $params")
|
||||
appsFlyerLib.logEvent(
|
||||
context,
|
||||
event,
|
||||
|
|
@ -78,21 +78,21 @@ class AppsFlyerClient @AssistedInject constructor(
|
|||
|
||||
private object InitializationListener : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.d("AppsFlyer initialized successfully")
|
||||
TangemLogger.d("AppsFlyer initialized successfully")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.e("AppsFlyer initialization error: $p0, $p1")
|
||||
TangemLogger.e("AppsFlyer initialization error: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
private object LogEventListener : AppsFlyerRequestListener {
|
||||
override fun onSuccess() {
|
||||
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
|
||||
TangemLogger.withTag("AppsFlyerClient").i("AppsFlyerRequestListener send")
|
||||
}
|
||||
|
||||
override fun onError(p0: Int, p1: String) {
|
||||
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
|
||||
TangemLogger.withTag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package com.tangem.tap.common.clipboard
|
|||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipDescription.MIMETYPE_TEXT_HTML
|
||||
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 +25,15 @@ 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")
|
||||
if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false &&
|
||||
!clipDescription.hasMimeType(MIMETYPE_TEXT_HTML)
|
||||
) {
|
||||
TangemLogger.d("Clipboard doesn't contain text")
|
||||
return default
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.tap.common.entities
|
||||
|
||||
import com.tangem.tap.common.toggleWidget.WidgetState
|
||||
|
||||
enum class ProgressState : WidgetState { Loading, Done, Error }
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -45,26 +40,6 @@ suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet) {
|
|||
state.globalState.tapWalletManager.onWalletSelected(userWallet)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fatal used to indicate errors that should not normally occur
|
||||
*/
|
||||
fun Store<AppState>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
|
||||
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
|
||||
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
|
||||
}
|
||||
|
||||
fun Store<AppState>.dispatchDebugErrorNotification(error: TapError) {
|
||||
inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error")))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
|
||||
dispatchOnMain(GlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
|
||||
fun Store<*>.dispatchDialogHide() {
|
||||
dispatchOnMain(GlobalAction.HideDialog)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch action inside a coroutine with the Main dispatcher
|
||||
*/
|
||||
|
|
@ -76,14 +51,6 @@ suspend fun dispatchOnMain(vararg actions: Action) {
|
|||
withMainContext { actions.forEach { store.dispatch(it) } }
|
||||
}
|
||||
|
||||
fun Store<AppState>.dispatchOpenUrl(url: String) {
|
||||
inject(DaggerGraphState::urlOpener).openUrl(url)
|
||||
}
|
||||
|
||||
fun Store<AppState>.dispatchShare(url: String) {
|
||||
inject(DaggerGraphState::shareManager).shareText(url)
|
||||
}
|
||||
|
||||
fun Store<AppState>.dispatchNavigationAction(action: AppRouter.() -> Unit) {
|
||||
inject(DaggerGraphState::appRouter).action()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionManager
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View {
|
||||
return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot)
|
||||
}
|
||||
|
||||
fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) {
|
||||
TransitionManager.beginDelayedTransition(this, transition)
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import com.tangem.tap.domain.TapError
|
|||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.delay
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -31,7 +31,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try
|
|||
Result.Success(wallet)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception)
|
||||
TangemLogger.e("Error", exception)
|
||||
|
||||
val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager)
|
||||
if (!networkConnectionManager.isOnline) {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import coil.decode.ImageDecoderDecoder
|
|||
import coil.decode.SvgDecoder
|
||||
import coil.memory.MemoryCache
|
||||
import coil.request.CachePolicy
|
||||
import coil.util.Logger
|
||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
import coil.util.Logger as CoilLogger
|
||||
|
||||
private const val COIL_LOG_TAG = "COIL"
|
||||
private const val COIL_MEMORY_CACHE_SIZE = 0.25
|
||||
|
|
@ -22,7 +22,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
|
|||
.apply {
|
||||
if (!logEnabled) return@apply
|
||||
|
||||
logger(CoilTimberLogger())
|
||||
logger(CoilKermitLogger())
|
||||
okHttpClient {
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(createNetworkLoggingInterceptor())
|
||||
|
|
@ -48,14 +48,16 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
|
|||
.build()
|
||||
}
|
||||
|
||||
private class CoilTimberLogger : Logger {
|
||||
private class CoilKermitLogger : CoilLogger {
|
||||
|
||||
override var level: Int = Log.DEBUG
|
||||
private val logger = TangemLogger.withTag(COIL_LOG_TAG)
|
||||
|
||||
override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) {
|
||||
with(Timber.tag(COIL_LOG_TAG)) {
|
||||
if (throwable != null) e(throwable, message)
|
||||
if (message != null) d(message)
|
||||
if (throwable != null) {
|
||||
logger.e(message ?: "<EMPTY>", throwable)
|
||||
} else if (message != null) {
|
||||
logger.d(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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+)+$")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class AppDialog : StateDialog {
|
||||
data class SimpleOkDialogRes(
|
||||
val headerId: Int,
|
||||
val messageId: Int,
|
||||
val args: List<String> = emptyList(),
|
||||
val onOk: VoidCallback? = null,
|
||||
) : AppDialog()
|
||||
|
||||
data class RemoveWalletDialog(
|
||||
val currencyTitle: String,
|
||||
val onOk: () -> Unit,
|
||||
) : AppDialog() {
|
||||
val messageRes: Int = R.string.token_details_hide_alert_message
|
||||
val titleRes: Int = R.string.token_details_hide_alert_title
|
||||
val primaryButtonRes: Int = R.string.token_details_hide_alert_hide
|
||||
}
|
||||
|
||||
data class TokensAreLinkedDialog(
|
||||
val currencyTitle: String,
|
||||
val currencySymbol: String,
|
||||
val networkName: String,
|
||||
) : AppDialog() {
|
||||
val messageRes: Int = R.string.token_details_unable_hide_alert_message
|
||||
val titleRes: Int = R.string.token_details_unable_hide_alert_title
|
||||
}
|
||||
|
||||
data class WalletAlreadyWasUsedDialog(
|
||||
val onOk: () -> Unit,
|
||||
val onSupportClick: () -> Unit,
|
||||
val onCancel: () -> Unit,
|
||||
) : AppDialog()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -9,7 +9,7 @@ import timber.log.Timber
|
|||
val logMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
Timber.i("Dispatch action: ${action::class.java.simpleName}")
|
||||
TangemLogger.i("Dispatch action: ${action::class.java.simpleName}")
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,11 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class GlobalAction : Action {
|
||||
|
||||
// dialogs
|
||||
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
|
||||
object HideDialog : GlobalAction()
|
||||
|
||||
object ScanFailsCounter {
|
||||
data class ChooseBehavior(
|
||||
val result: CompletionResult<ScanResponse>,
|
||||
val analyticsSource: AnalyticsParam.ScreensSources,
|
||||
) : GlobalAction()
|
||||
|
||||
object Reset : GlobalAction()
|
||||
object Increment : GlobalAction()
|
||||
}
|
||||
|
||||
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
|
||||
|
||||
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
package com.tangem.tap.common.redux.global
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -31,41 +25,12 @@ private val globalMiddlewareHandler: Middleware<AppState> = { _, _ ->
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod")
|
||||
private fun handleAction(action: Action) {
|
||||
when (action) {
|
||||
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
|
||||
when (action.result) {
|
||||
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
is CompletionResult.Failure -> {
|
||||
handleFailureChooseBehaviour(action.result, action.analyticsSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.RestoreAppCurrency -> restoreAppCurrency()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFailureChooseBehaviour(
|
||||
result: CompletionResult.Failure<ScanResponse>,
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
) {
|
||||
if (result.error is TangemSdkError.UserCancelled) {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
|
||||
if (store.state.globalState.scanCardFailsCounter >= 2) {
|
||||
val scanFailsSource = when (analyticsSource) {
|
||||
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
|
||||
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
|
||||
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
|
||||
else -> StateDialog.ScanFailsSource.MAIN
|
||||
}
|
||||
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
|
||||
}
|
||||
} else {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreAppCurrency() {
|
||||
scope.launch {
|
||||
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
val globalState = state.globalState
|
||||
|
||||
return when (action) {
|
||||
is GlobalAction.ScanFailsCounter.Increment -> {
|
||||
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
|
||||
}
|
||||
is GlobalAction.ScanFailsCounter.Reset -> {
|
||||
globalState.copy(scanCardFailsCounter = 0)
|
||||
}
|
||||
is GlobalAction.SaveScanResponse -> {
|
||||
globalState.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
|
|
@ -26,12 +20,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
globalState.copy(appCurrency = action.appCurrency)
|
||||
}
|
||||
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
|
||||
is GlobalAction.ShowDialog -> {
|
||||
globalState.copy(dialog = action.stateDialog)
|
||||
}
|
||||
is GlobalAction.HideDialog -> {
|
||||
globalState.copy(dialog = null)
|
||||
}
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,15 @@ package com.tangem.tap.common.redux.global
|
|||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class GlobalState(
|
||||
@Deprecated("Use scan response from selected user wallet")
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val onboardingState: OnboardingState = OnboardingState(),
|
||||
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
||||
val appCurrency: AppCurrency = AppCurrency.Default,
|
||||
val scanCardFailsCounter: Int = 0,
|
||||
val dialog: StateDialog? = null,
|
||||
val isLastSignWithRing: Boolean = false,
|
||||
) : StateType
|
||||
|
||||
typealias CryptoCurrencyName = String
|
||||
|
||||
data class OnboardingState(
|
||||
val isOnboardingStarted: Boolean = false,
|
||||
val shouldResetOnCreate: Boolean = false,
|
||||
)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.tap.common.toggleWidget
|
||||
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface WidgetState
|
||||
|
||||
interface ViewStateWidget {
|
||||
val mainView: View
|
||||
fun changeState(state: WidgetState)
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.common.transitions
|
||||
|
||||
import androidx.transition.ChangeBounds
|
||||
import androidx.transition.ChangeTransform
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionSet
|
||||
|
||||
class HomeToOnboardingTransition : TransitionSet() {
|
||||
init {
|
||||
ordering = ORDERING_TOGETHER
|
||||
addTransition(Fade())
|
||||
addTransition(ChangeTransform())
|
||||
addTransition(ChangeBounds())
|
||||
}
|
||||
}
|
||||
|
||||
class InternalNoteLayoutTransition : TransitionSet() {
|
||||
init {
|
||||
ordering = ORDERING_TOGETHER
|
||||
addTransition(ChangeTransform())
|
||||
addTransition(ChangeBounds())
|
||||
addTransition(Fade())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package com.tangem.tap.common.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.core.view.isVisible
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object ScanFailsDialog {
|
||||
|
||||
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
|
||||
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
|
||||
private const val RUSSIA_LOCALE = "ru"
|
||||
|
||||
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
|
||||
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
|
||||
val customView = View.inflate(context, R.layout.dialog_scan_fails, null)
|
||||
val sourceAnalytics = when (source) {
|
||||
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
|
||||
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
|
||||
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
|
||||
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
|
||||
}
|
||||
val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button)
|
||||
if (onTryAgain != null) {
|
||||
tryAgainBtn?.isVisible = true
|
||||
tryAgainBtn?.setOnClickListener {
|
||||
store.dispatchDialogHide()
|
||||
Analytics.send(
|
||||
ScanFailsDialogAnalytics(
|
||||
button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN,
|
||||
source = sourceAnalytics,
|
||||
),
|
||||
)
|
||||
onTryAgain()
|
||||
}
|
||||
} else {
|
||||
tryAgainBtn?.isVisible = false
|
||||
}
|
||||
customView.findViewById<TextView>(R.id.how_to_scan_button)?.setOnClickListener {
|
||||
Analytics.send(
|
||||
ScanFailsDialogAnalytics(
|
||||
button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN,
|
||||
source = sourceAnalytics,
|
||||
),
|
||||
)
|
||||
val locale = Locale.current.region
|
||||
val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
|
||||
store.dispatchOpenUrl(link)
|
||||
}
|
||||
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
|
||||
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
|
||||
|
||||
scope.launch {
|
||||
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
|
||||
.invoke(type = FeedbackEmailType.ScanningProblem)
|
||||
}
|
||||
}
|
||||
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
|
||||
store.dispatchDialogHide()
|
||||
}
|
||||
setView(customView)
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
package com.tangem.tap.core.security
|
||||
|
||||
import android.os.Build
|
||||
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
|
||||
|
|
@ -12,12 +13,86 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
|
|||
override val isXposed: Boolean
|
||||
get() = getRtcStatusSafely()?.xposed == true
|
||||
|
||||
override val isVulnerableToMediaTekExploit: Boolean by lazy {
|
||||
val isAffected by lazy { isAffectedMediaTekDevice() }
|
||||
val isPatched by lazy { hasSecurityPatch() }
|
||||
val isVulnerable = isAffected && !isPatched
|
||||
TangemLogger.i(
|
||||
"CVE-2026-20435 check: isAffectedMediaTek=$isAffected, " +
|
||||
"isPatched=$isPatched, isVulnerable=$isVulnerable",
|
||||
)
|
||||
isVulnerable
|
||||
}
|
||||
|
||||
private fun isAffectedMediaTekDevice(): Boolean {
|
||||
val socModel = resolveMediaTekSocModel()
|
||||
val isAffected = socModel != null && socModel in AFFECTED_MEDIATEK_SOCS
|
||||
TangemLogger.i("CVE-2026-20435 SoC result: model=$socModel, isAffected=$isAffected")
|
||||
return isAffected
|
||||
}
|
||||
|
||||
private fun resolveMediaTekSocModel(): String? {
|
||||
// Layer 1: API 31+ provides direct SoC info (public API, most reliable)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val manufacturer = Build.SOC_MANUFACTURER
|
||||
val model = Build.SOC_MODEL
|
||||
TangemLogger.i("CVE-2026-20435 Layer 1: SOC_MANUFACTURER=$manufacturer, SOC_MODEL=$model")
|
||||
if (manufacturer.equals("MediaTek", ignoreCase = true)) {
|
||||
extractSocModel(model)?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 2: Build.HARDWARE often contains "mtXXXX" on MediaTek devices (public API)
|
||||
val hardware = Build.HARDWARE
|
||||
TangemLogger.i("CVE-2026-20435 Layer 2: HARDWARE=$hardware")
|
||||
extractSocModel(hardware)?.let { return it }
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun extractSocModel(value: String): String? {
|
||||
val match = MEDIATEK_SOC_PATTERN.find(value.uppercase()) ?: return null
|
||||
return match.value
|
||||
}
|
||||
|
||||
private fun hasSecurityPatch(): Boolean {
|
||||
val patch = Build.VERSION.SECURITY_PATCH
|
||||
val isPatched = try {
|
||||
patch >= MEDIATEK_CVE_FIX_PATCH_LEVEL
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.w("CVE-2026-20435 patch check: failed to parse SECURITY_PATCH=$patch", e)
|
||||
false // fail-safe: treat unknown patch level as unpatched
|
||||
}
|
||||
TangemLogger.i(
|
||||
"CVE-2026-20435 patch check: SECURITY_PATCH=$patch, " +
|
||||
"required=$MEDIATEK_CVE_FIX_PATCH_LEVEL, isPatched=$isPatched",
|
||||
)
|
||||
return isPatched
|
||||
}
|
||||
|
||||
private fun getRtcStatusSafely(): RtcStatus? {
|
||||
return try {
|
||||
RtcStatus.getRtcStatus()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e)
|
||||
TangemLogger.e("Error", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Android security patch level that includes the fix for CVE-2026-20435 */
|
||||
const val MEDIATEK_CVE_FIX_PATCH_LEVEL = "2026-03-05"
|
||||
|
||||
/** Regex to extract MediaTek SoC model number (e.g., MT6789) */
|
||||
val MEDIATEK_SOC_PATTERN = Regex("MT\\d{4}")
|
||||
|
||||
/** Affected MediaTek SoC models per Ledger Donjon disclosure */
|
||||
val AFFECTED_MEDIATEK_SOCS = setOf(
|
||||
"MT6739", "MT6761", "MT6765", "MT6768", "MT6781",
|
||||
"MT6789", "MT6813", "MT6833", "MT6853", "MT6855",
|
||||
"MT6877", "MT6878", "MT6879", "MT6880", "MT6885",
|
||||
"MT6886", "MT6890", "MT6893", "MT6895", "MT6897",
|
||||
"MT6983", "MT6985", "MT6989", "MT6990", "MT6993",
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue