Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-24 00:07:05 +03:00
commit edec3fcdff
391 changed files with 10988 additions and 3221 deletions

View file

@ -113,12 +113,14 @@ dependencies {
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.models)
implementation(projects.domain.core)
api(projects.domain.common)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.demo.models)
implementation(projects.domain.express)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)

View file

@ -5,7 +5,10 @@ object TestConstants {
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val CARDANO_ADDRESS =
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
const val POLKADOT_RECIPIENT_ADDRESS = "143TfgFYAFfM86LRzt4UcFNU3KosxCndBCVz2U5HCxpLidKZ"
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L

View file

@ -20,12 +20,12 @@ fun BaseTestCase.swipeVertical(
)
}
fun BaseTestCase.pullToRefresh() {
fun BaseTestCase.pullToRefresh(steps: Int = 1000) {
swipeVertical(
direction = SwipeDirection.DOWN,
startHeightRatio = 0.2f,
endHeightRatio = 0.8f,
steps = 1000
steps = steps
)
}

View file

@ -115,4 +115,28 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() }
}
}
fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) {
if (isEnabled) {
step("Assert 'Buy' button is enabled") {
onMainScreen { buyButton.assertIsEnabled() }
}
step("Assert 'Swap' button is enabled") {
onMainScreen { swapButton.assertIsEnabled() }
}
step("Assert 'Sell' button is enabled") {
onMainScreen { sellButton.assertIsEnabled() }
}
} else {
step("Assert 'Buy' button is not enabled") {
onMainScreen { buyButton.assertIsNotEnabled() }
}
step("Assert 'Swap' button is not enabled") {
onMainScreen { swapButton.assertIsNotEnabled() }
}
step("Assert 'Sell' button is not enabled") {
onMainScreen { sellButton.assertIsNotEnabled() }
}
}
}

View file

@ -8,6 +8,7 @@ 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.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step
@ -63,4 +64,16 @@ fun checkAlreadyUsedWalletDialog() {
step("Assert 'Request support' button is displayed") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() }
}
}
fun BaseTestCase.checkActionIsUnavailableDialog() {
step("Assert 'Action is unavailable' dialog title is displayed") {
onActionIsUnavailableDialog { title.assertIsDisplayed() }
}
step("Assert 'Action is unavailable' dialog text is displayed") {
onActionIsUnavailableDialog { text.assertIsDisplayed() }
}
step("Assert 'Action is unavailable' dialog 'Ok' button is displayed") {
onActionIsUnavailableDialog { okButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.screens.onSendConfirmScreen
import io.github.kakaocup.compose.node.element.KNode
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkSendWarning(
titleResId: Int,
messageResId: Int,
amount: String,
isDisplayed: Boolean = true,
) {
val assertDisplay = if (isDisplayed) "displayed" else "not displayed"
step("Assert 'Send confirm screen' is displayed") {
onSendConfirmScreen {
title.assertIsDisplayed()
}
}
step("Assert warning title is $assertDisplay") {
onSendConfirmScreen {
warningTitle(titleResId).assertVisibility(isDisplayed)
}
}
step("Assert warning icon is $assertDisplay") {
onSendConfirmScreen {
sendWarningIcon(messageResId, amount).assertVisibility(isDisplayed)
}
}
step("Assert warning message is $assertDisplay") {
onSendConfirmScreen {
sendWarningMessage(messageResId, amount).assertVisibility(isDisplayed)
}
}
if (isDisplayed)
step("Assert 'Send' button is disabled") {
onSendConfirmScreen {
sendButton.assertIsNotEnabled()
}
}
else
step("Assert 'Send' button is enabled") {
onSendConfirmScreen {
sendButton.assertIsEnabled()
}
}
}
private fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
if (shouldBeDisplayed) {
assertIsDisplayed()
} else {
assertIsNotDisplayed()
}
}

View file

@ -0,0 +1,40 @@
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
import com.tangem.common.ui.R as CommonUIR
class ActionIsUnavailableDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ActionIsUnavailableDialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
hasText(getResourceString(CommonUIR.string.action_buttons_something_wrong_alert_title))
useUnmergedTree = true
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.action_buttons_something_wrong_alert_message))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onActionIsUnavailableDialog(function: ActionIsUnavailableDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -17,6 +17,14 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
}
val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel))

View file

@ -0,0 +1,24 @@
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.*
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 SellPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SellPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_sell))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSellScreen(function: SellPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -2,17 +2,13 @@ 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.NotificationTestTags
import com.tangem.core.ui.test.SendConfirmScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.core.ui.test.*
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
import com.tangem.common.ui.R as CommonUiR
class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
@ -23,15 +19,25 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
val sendButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_send))
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.common_send)))
useUnmergedTree = true
}
val primaryAmount: KNode = child {
hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val minimumSendAmountErrorTitle: KNode = child {
fun leaveDepositButton(amount: String): KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.send_notification_leave_button, amount)))
useUnmergedTree = true
}
fun warningTitle(titleResId: Int): KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(CommonUiR.string.send_notification_invalid_amount_title))
hasText(getResourceString(titleResId))
useUnmergedTree = true
}
@ -40,34 +46,35 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true
}
fun minimumSendAmountErrorIcon(amount: String): KNode = child {
fun sendWarningIcon(messageResId: Int, amount: String): KNode = child {
hasTestTag(NotificationTestTags.ICON)
hasAnySibling(
withText(
getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text,
messageResId,
amount,
amount,
)
)
)
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true
}
fun minimumSendAmountErrorMessage(
fun sendWarningMessage(
messageResId: Int,
amount: String,
): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(
getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text,
messageResId,
amount,
amount,
)
)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =

View file

@ -64,6 +64,12 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_continue))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) =

View file

@ -4,11 +4,16 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.pullToRefresh
import com.tangem.common.ui.R
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -29,6 +34,9 @@ class BlockchainTest : BaseTestCase() {
val scenarioName = "user_tokens_api"
val scenarioState = "Cardano"
val invalidAmountTitleResId = R.string.send_notification_invalid_amount_title
val invalidAmountMessageResId = R.string.send_notification_invalid_minimum_amount_text
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
@ -64,14 +72,12 @@ class BlockchainTest : BaseTestCase() {
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' error title is displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsDisplayed() }
}
step("Assert 'Invalid amount' error icon is displayed") {
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsDisplayed() }
}
step("Assert 'Invalid amount' error message is displayed") {
onSendConfirmScreen { minimumSendAmountErrorMessage(minAmount).assertIsDisplayed() }
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = minAmount
)
}
step("Press system 'Back' button") {
device.uiDevice.pressBack()
@ -97,14 +103,13 @@ class BlockchainTest : BaseTestCase() {
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount' error title is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsNotDisplayed() }
}
step("Assert 'Invalid amount' error icon is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsNotDisplayed() }
}
step("Assert 'Invalid amount' error message is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorMessage(minAmount).assertIsNotDisplayed() }
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = minAmount,
isDisplayed = false
)
}
}
}
@ -136,10 +141,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountInfoScenarioName, state = rippleAccountInfoErrorState)
setWireMockScenarioState(
scenarioName = rippleAccountInfoScenarioName,
state = rippleAccountInfoErrorState
)
}
step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesErrorState)
setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesErrorState
)
}
step("Open 'Main Screen'") {
openMainScreen()
@ -169,10 +180,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountInfoScenarioName, state = rippleAccountInfoStartedState)
setWireMockScenarioState(
scenarioName = rippleAccountInfoScenarioName,
state = rippleAccountInfoStartedState
)
}
step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesStartedState)
setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesStartedState
)
}
step("Pull to refresh") {
pullToRefresh()

View file

@ -5,11 +5,18 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.*
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.assertActionButtonsForMultiCurrencyWallet
import com.tangem.scenarios.checkActionIsUnavailableDialog
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.content.TwinsMockContent
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -391,4 +398,258 @@ class MainScreenActionButtonsTest : BaseTestCase() {
}
}
}
@AllureId("895")
@DisplayName("Action buttons: check blockchain information by click on 'Buy' button")
@Test
fun checkClickOnBuyButtonOnMainTest() {
val cardType: MockContent = TwinsMockContent
val cardName = "Twin"
val tokenTitle = "Bitcoin"
val tokenSymbol = "BTC"
setupHooks().run {
step("Open 'Main Screen' on '$cardName' card") {
openMainScreen(mockContent = cardType, isTwinsCard = true)
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Click on 'Confirm' button in 'Dialog'") {
waitForIdle()
onDialog { confirmButton.clickWithAssertion() }
}
step("Assert top app bar title contains '$tokenTitle'") {
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
}
step("Assert fiat currency text field is displayed") {
onBuyTokenDetailsScreen { fiatAmountTextField.assertIsDisplayed() }
}
step("Assert fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
}
step("Assert token amount field is displayed") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenSymbol, substring = true) }
}
step("Assert 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.assertIsDisplayed() }
}
}
}
@AllureId("4395")
@DisplayName("Action buttons (main screen): click on buttons with success response")
@Test
fun clickOnActionButtonsWithSuccessResponseTest() {
val tokenTitle = "Ethereum"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Assert 'Buy' screen title is displayed") {
onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() }
}
step("Assert token with title: '$tokenTitle' is displayed") {
onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Click on close button on stories screen") {
onSwapStoriesScreen { closeButton.performClick() }
}
step("Assert 'Swap' token screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Assert 'Sell' token screen title is displayed") {
onSellScreen { title.assertIsDisplayed() }
}
}
}
@AllureId("4396")
@DisplayName("Action buttons (main screen): click on buttons without data")
@Test
fun clickOnActionButtonsWithoutDataTest() {
setupHooks(
additionalAfterSection = {
enableWiFi()
enableMobileData()
}
).run {
step("Turn off internet") {
disableWiFi()
disableMobileData()
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
}
}
@AllureId("4398")
@DisplayName("Action buttons (main screen): click on buttons with error response")
@Test
fun clickOnActionButtonsWithErrorResponseTest() {
val scenarioName = "express_api_assets"
val scenarioState = "Error"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
// ToDo("[REDACTED_JIRA] - add ability to use MoonPay mocks")
// step("Check 'Action is unavailable' dialog") {
// checkActionIsUnavailableDialog()
// }
// step("Click on 'Ok' button") {
// onDialog { okButton.performClick() }
// }
}
}
@AllureId("3642")
@DisplayName("Action buttons (main screen): check buttons state")
@Test
fun checkButtonsStateTest() {
val scenarioName = "user_tokens_api"
val scenarioState = "EmptyTokensList"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
step("Reset Wiremock scenario: '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = true)
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
}
}
}

View file

@ -0,0 +1,249 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import com.tangem.wallet.R
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 SolanaWarningsTest : BaseTestCase() {
private val tokenName = "Solana"
private val amountToLeaveLessThanRent = "0.0016941"
private val amountToLeaveGreaterThanRent = "0.0000941"
private val amountToLeaveRentOnly = "0.00168934"
private val userTokensScenarioName = "user_tokens_api"
private val userTokensScenarioState = "Solana"
private val quotesScenarioName = "quotes_api"
private val quotesScenarioState = "Solana"
private val rentAmount = "0.000890880"
private val invalidAmountTitleResId = R.string.send_notification_invalid_amount_title
private val invalidAmountMessageResId = R.string.send_notification_invalid_amount_rent_fee
@AllureId("564")
@DisplayName("Warnings: warning is displayed, if after send balance is less than rent amount (SOLANA)")
@Test
fun warningIsDisplayedWhenLeaveLessThanRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenarioName)
resetWireMockScenarioState(quotesScenarioName)
}
).run {
step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$quotesScenarioName' to state: '$quotesScenarioState'") {
setWireMockScenarioState(scenarioName = quotesScenarioName, state = quotesScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
}
step("Type '$amountToLeaveLessThanRent' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanRent)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount
)
}
}
}
@AllureId("567")
@DisplayName("Warnings: warning is not displayed, if after send balance is greater than rent amount (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveGreaterThanRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenarioName)
resetWireMockScenarioState(quotesScenarioName)
}
).run {
step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$quotesScenarioName' to state: '$quotesScenarioState'") {
setWireMockScenarioState(scenarioName = quotesScenarioName, state = quotesScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
}
step("Type '$amountToLeaveGreaterThanRent' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanRent)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount,
isDisplayed = false
)
}
}
}
@AllureId("566")
@DisplayName("Warnings: warning is not displayed, if after send balance is equal to rent amount (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveOnlyRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenarioName)
resetWireMockScenarioState(quotesScenarioName)
}
).run {
step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$quotesScenarioName' to state: '$quotesScenarioState'") {
setWireMockScenarioState(scenarioName = quotesScenarioName, state = quotesScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
}
step("Type '$amountToLeaveRentOnly' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveRentOnly)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount,
isDisplayed = false
)
}
}
}
@AllureId("565")
@DisplayName("Warnings: warning is not displayed, if after send balance is zero (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveZeroSol() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenarioName)
resetWireMockScenarioState(quotesScenarioName)
}
).run {
step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Set WireMock scenario: '$quotesScenarioName' to state: '$quotesScenarioState'") {
setWireMockScenarioState(scenarioName = quotesScenarioName, state = quotesScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() }
}
step("Type max amount in input text field") {
onSendScreen {
maxButton.performClick()
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount,
isDisplayed = false
)
}
}
}
}

View file

@ -3,10 +3,13 @@ package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.addHotWalletContext
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.setHotWalletContext
/**
[REDACTED_AUTHOR]
@ -17,6 +20,14 @@ internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
Analytics.setContext(scanResponse)
}
override fun addContext(userWallet: UserWallet) {
Analytics.addContext(userWallet)
}
override fun setHotWalletContext() {
Analytics.setHotWalletContext()
}
override fun eraseContext() {
Analytics.eraseContext()
}
@ -25,6 +36,10 @@ internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
Analytics.addContext(scanResponse)
}
override fun addHotWalletContext() {
Analytics.addHotWalletContext()
}
override fun removeContext() {
Analytics.removeContext()
}

View file

@ -6,6 +6,7 @@ class BlockchainApiExceptionEvent(
selectedHost: String,
exceptionHost: String,
error: String,
blockchain: String,
) : AnalyticsEvent(
category = "BlockchainSdk",
event = "Exception",
@ -13,5 +14,6 @@ class BlockchainApiExceptionEvent(
AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost,
AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost,
AnalyticsParam.ERROR_DESCRIPTION to error,
AnalyticsParam.BLOCKCHAIN to blockchain,
),
)

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.analytics.handlers
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.ExceptionHandlerOutput
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent
import javax.inject.Inject
@ -8,12 +10,13 @@ import javax.inject.Inject
class BlockchainExceptionHandler @Inject constructor(
private val analyticsErrorHandler: AnalyticsErrorHandler,
) : ExceptionHandlerOutput {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String, blockchain: Blockchain) {
analyticsErrorHandler.sendErrorEvent(
BlockchainApiExceptionEvent(
selectedHost = nextHost,
exceptionHost = currentHost,
error = message,
blockchain = blockchain.toNetworkId(),
),
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null,
) : ParamsInterceptor {
override fun id(): String = HotWalletContextInterceptor.id()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.PRODUCT_TYPE] = "Mobile Wallet"
}
companion object {
fun id(): String = HotWalletContextInterceptor::class.java.simpleName
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.domain.models.scan.ScanResponse
*/
class LinkedCardContextInterceptor(
scanResponse: ScanResponse,
val parent: LinkedCardContextInterceptor? = null,
val parent: ParamsInterceptor? = null,
) : ParamsInterceptor {
private val contextInterceptor = CardContextInterceptor(scanResponse)

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.HotWalletContextInterceptor
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/**
@ -24,19 +25,46 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
fun Analytics.setContext(userWallet: UserWallet) {
setUserId(userWallet.walletId.stringValue)
// TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
if (userWallet is UserWallet.Cold) {
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
when (userWallet) {
is UserWallet.Cold -> {
removeParamsInterceptor(HotWalletContextInterceptor.id())
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
}
is UserWallet.Hot -> {
removeParamsInterceptor(LinkedCardContextInterceptor.id())
addParamsInterceptor(HotWalletContextInterceptor())
}
}
}
fun Analytics.setHotWalletContext() {
addParamsInterceptor(HotWalletContextInterceptor())
}
/**
* Erases the context
*/
fun Analytics.eraseContext() {
clearUserId()
removeParamsInterceptor(LinkedCardContextInterceptor.id())
removeParamsInterceptor(HotWalletContextInterceptor.id())
}
/**
* Adds a new context and keeps a previous context as the parent of the new one
*/
fun Analytics.addContext(userWallet: UserWallet) {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = when (userWallet) {
is UserWallet.Cold -> LinkedCardContextInterceptor(userWallet.scanResponse, parent = currentContext)
is UserWallet.Hot -> HotWalletContextInterceptor(parent = currentContext)
}
setUserId(userWalletId = userWallet.walletId.stringValue)
addParamsInterceptor(newContext)
}
/**
@ -48,18 +76,32 @@ fun Analytics.addContext(scanResponse: ScanResponse) {
setUserId(userWalletId.stringValue)
}
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)
addParamsInterceptor(newContext)
}
fun Analytics.addHotWalletContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val newContext = HotWalletContextInterceptor(currentContext)
addParamsInterceptor(newContext)
}
/**
* Removes the current context and restores the previous one if it was present.
*/
fun Analytics.removeContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val previousContext = currentContext?.parent ?: return
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val previousContext = when (currentContext) {
is LinkedCardContextInterceptor -> currentContext.parent
is HotWalletContextInterceptor -> currentContext.parent
else -> null
} ?: return
addParamsInterceptor(previousContext)
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.common.extensions
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
// TODO: move extensions to utils
fun BigDecimal.toFormattedString(
decimals: Int,
roundingMode: RoundingMode = RoundingMode.DOWN,
locale: Locale = Locale.US,
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat()
df.decimalFormatSymbols = symbols
df.maximumFractionDigits = decimals
df.minimumFractionDigits = 0
df.isGroupingUsed = true
df.roundingMode = roundingMode
return df.format(this)
}

View file

@ -1,10 +1,9 @@
package com.tangem.tap.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -46,17 +45,15 @@ internal object ActivityModule {
@Singleton
fun provideDefaultRampManager(
appStateHolder: AppStateHolder,
expressServiceLoader: ExpressServiceLoader,
expressServiceFetcher: ExpressServiceFetcher,
currenciesRepository: CurrenciesRepository,
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider,
): RampStateManager {
return DefaultRampManager(
sellService = Provider { requireNotNull(appStateHolder.sellService) },
expressServiceLoader = expressServiceLoader,
expressServiceFetcher = expressServiceFetcher,
currenciesRepository = currenciesRepository,
dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.*
import dagger.Module
@ -50,10 +52,12 @@ internal object AccountDomainModule {
fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
)
}

View file

@ -179,20 +179,6 @@ internal object OnrampDomainModule {
)
}
@Provides
@Singleton
fun provideGetOnrampV2QuotesUseCase(
settingsRepository: SettingsRepository,
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampV2QuotesUseCase {
return GetOnrampV2QuotesUseCase(
settingsRepository = settingsRepository,
repository = onrampRepository,
errorResolver = onrampErrorResolver,
)
}
@Provides
@Singleton
fun provideGetOnrampProviderWithQuoteUseCase(

View file

@ -20,11 +20,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -60,24 +56,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideFetchPendingTransactionsUseCase(
@ -184,24 +162,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyUseCase(

View file

@ -109,7 +109,7 @@ object BackupWalletMockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -185,7 +185,7 @@ object BackupWalletMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -244,7 +244,7 @@ object BackupWalletMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -109,7 +109,7 @@ object DevWalletMockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -185,7 +185,7 @@ object DevWalletMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -244,7 +244,7 @@ object DevWalletMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -105,7 +105,7 @@ object Firmware412MockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -181,7 +181,7 @@ object Firmware412MockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -240,7 +240,7 @@ object Firmware412MockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -109,7 +109,7 @@ object RingMockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -185,7 +185,7 @@ object RingMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -244,7 +244,7 @@ object RingMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -109,7 +109,7 @@ object ShibaMockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -185,7 +185,7 @@ object ShibaMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -244,7 +244,7 @@ object ShibaMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -109,7 +109,7 @@ object ShibaNoBackupMockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -185,7 +185,7 @@ object ShibaNoBackupMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -244,7 +244,7 @@ object ShibaNoBackupMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -138,7 +138,7 @@ object ShibaNoBackupNoWalletsMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -197,7 +197,7 @@ object ShibaNoBackupNoWalletsMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -229,7 +229,7 @@ object Wallet2MockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -281,7 +281,7 @@ object Wallet2MockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -229,7 +229,7 @@ object Wallet2NoBackupMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -281,7 +281,7 @@ object Wallet2NoBackupMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -141,7 +141,7 @@ object Wallet2NoBackupNoWalletsMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -193,7 +193,7 @@ object Wallet2NoBackupNoWalletsMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -229,7 +229,7 @@ object Wallet2WithSeedPhraseMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -281,7 +281,7 @@ object Wallet2WithSeedPhraseMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),

View file

@ -110,7 +110,7 @@ object WalletMockContent : MockContent {
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
@ -125,10 +125,18 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
@ -194,7 +202,7 @@ object WalletMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -235,6 +243,13 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/148'/0'") to ExtendedPublicKey( // XLM
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
ByteArrayKey(
@ -244,14 +259,21 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey( // Solana
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -290,14 +312,14 @@ object WalletMockContent : MockContent {
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // xrp
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -312,19 +334,26 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey( // Solana
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),

View file

@ -17,7 +17,6 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
*
[REDACTED_AUTHOR]
*/
// TODO remove it after test after resolve [REDACTED_JIRA]
internal class ResetBackupCardTask(
private val userWalletId: UserWalletId,
) : CardSessionRunnable<Boolean> {

View file

@ -2,44 +2,14 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.utils.converter.Converter
internal class CryptoCurrencyConverter(
private val excludedBlockchains: ExcludedBlockchains,
) : TwoWayConverter<Currency, CryptoCurrency> {
internal object CryptoCurrencyConverter : Converter<CryptoCurrency, Currency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) }
override fun convert(value: Currency): CryptoCurrency {
return when (value) {
is Currency.Blockchain -> requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = getSelectedWallet(),
),
)
is Currency.Token -> requireNotNull(
cryptoCurrencyFactory.createToken(
sdkToken = value.token,
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = getSelectedWallet(),
),
)
}
}
override fun convertBack(value: CryptoCurrency): Currency {
override fun convert(value: CryptoCurrency): Currency {
val blockchain = value.network.toBlockchain()
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) {
@ -60,15 +30,4 @@ internal class CryptoCurrencyConverter(
)
}
}
fun getSelectedWallet(): UserWallet {
val userWalletListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
requireNotNull(userWalletsListRepository.selectedUserWallet.value)
} else {
requireNotNull(userWalletListManager.selectedUserWalletSync)
}
}
}

View file

@ -5,13 +5,11 @@ import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.right
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
@ -26,17 +24,13 @@ import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
@Suppress("LongParameterList")
internal class DefaultRampManager(
private val sellService: Provider<ExchangeService>,
private val expressServiceLoader: ExpressServiceLoader,
private val expressServiceFetcher: ExpressServiceFetcher,
private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
override suspend fun availableForBuy(
userWallet: UserWallet,
cryptoCurrency: CryptoCurrency,
@ -56,7 +50,7 @@ internal class DefaultRampManager(
return either {
val isSellSupportedByService = catch(
block = {
val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency)
val serviceCurrency = CryptoCurrencyConverter.convert(status.currency)
sellService().availableForSell(currency = serviceCurrency)
},
@ -107,7 +101,7 @@ internal class DefaultRampManager(
}
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
return expressServiceLoader.getInitializationStatus(userWalletId)
return expressServiceFetcher.getInitializationStatus(userWalletId)
}
override suspend fun getSendUnavailabilityReason(
@ -151,14 +145,14 @@ internal class DefaultRampManager(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading
return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.exchangeAvailable?.toSwapAvailabilityState()
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) }
foundAsset?.isExchangeAvailable?.toSwapAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
}
@ -168,15 +162,15 @@ internal class DefaultRampManager(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull()
val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading
return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) }
foundAsset?.onrampAvailable?.toOnrampAvailabilityState()
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) }
foundAsset?.isOnrampAvailable?.toOnrampAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound
}
}
@ -211,8 +205,13 @@ internal class DefaultRampManager(
}
}
private fun CryptoCurrency.findAssetPredicate(asset: Asset): Boolean {
val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true)
private fun CryptoCurrency.findAssetPredicate(assetId: ExpressAsset.ID): Boolean {
val currencyAssedId = ExpressAsset.ID(
networkId = this.network.backendId,
contractAddress = (this as? CryptoCurrency.Token)?.contractAddress,
)
return assetId.networkId == currencyAssedId.networkId &&
assetId.contractAddress.equals(currencyAssedId.contractAddress, ignoreCase = true)
}
}

View file

@ -160,6 +160,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Pepecoin, PepecoinTestnet -> null
Hyperliquid, HyperliquidTestnet -> null
Quai, QuaiTestnet -> null
// Linea, LineaTestnet -> null
// ArbitrumNova -> null
Linea, LineaTestnet -> null
ArbitrumNova -> null
}

View file

@ -36,9 +36,10 @@ import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -62,6 +63,7 @@ internal class ChildFactory @Inject constructor(
private val detailsComponentFactory: DetailsComponent.Factory,
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val walletBackupComponentFactory: WalletBackupComponent.Factory,
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
@ -100,6 +102,7 @@ internal class ChildFactory @Inject constructor(
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory,
private val createHardwareWalletComponentFactory: CreateHardwareWalletComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
@ -107,8 +110,9 @@ internal class ChildFactory @Inject constructor(
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val viewPhraseComponentFactory: ViewPhraseComponent.Factory,
private val forgetWalletComponentFactory: ForgetWalletComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
@ -138,7 +142,6 @@ internal class ChildFactory @Inject constructor(
is AppRoute.ManageTokens -> {
val source = when (route.source) {
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
}
@ -187,6 +190,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletBackupComponentFactory,
)
}
is AppRoute.WalletHardwareBackup -> {
createComponentChild(
context = context,
params = WalletHardwareBackupComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = walletHardwareBackupComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
createComponentChild(
context = context,
@ -293,7 +305,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = StakingComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrencyId = route.cryptoCurrencyId,
cryptoCurrency = route.cryptoCurrency,
yieldId = route.yieldId,
),
componentFactory = stakingComponentFactory,
@ -496,6 +508,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = createWalletSelectionComponentFactory,
)
}
is AppRoute.CreateHardwareWallet -> {
createComponentChild(
context = context,
params = Unit,
componentFactory = createHardwareWalletComponentFactory,
)
}
is AppRoute.CreateMobileWallet -> {
createComponentChild(
context = context,
@ -533,6 +552,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId,
isUpgradeFlow = route.isUpgradeFlow,
),
componentFactory = createWalletBackupComponentFactory,
)
@ -555,6 +575,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = viewPhraseComponentFactory,
)
}
is AppRoute.ForgetWallet -> {
createComponentChild(
context = context,
params = ForgetWalletComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = forgetWalletComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,
@ -604,8 +633,11 @@ internal class ChildFactory @Inject constructor(
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(config = route.config),
componentFactory = tangemPayDetailsComponentFactory,
params = TangemPayDetailsContainerComponent.Params(
userWalletId = route.userWalletId,
config = route.config,
),
componentFactory = tangemPayDetailsContainerComponentFactory,
)
}
is AppRoute.TangemPayOnboarding -> {

View file

@ -126,9 +126,12 @@ sealed class AppRoute(val path: String) : Route {
val portfolioId: PortfolioId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
/**
* Source of launching the screen.
* ManageTokens screen launched from Onboarding by another route. See `OnboardingRoute.ManageTokens`.
*/
enum class Source {
STORIES,
ONBOARDING,
SETTINGS,
}
}
@ -192,9 +195,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Staking(
val userWalletId: UserWalletId,
val cryptoCurrencyId: CryptoCurrency.ID,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId")
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
@Serializable
data class PushNotification(
@ -217,6 +220,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}")
@Serializable
data class WalletHardwareBackup(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_hardware_backup/${userWalletId.stringValue}")
@Serializable
data object Markets : AppRoute(path = "/markets")
@ -322,6 +330,9 @@ sealed class AppRoute(val path: String) : Route {
}
}
@Serializable
object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet")
@Serializable
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")
@ -341,6 +352,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class CreateWalletBackup(
val userWalletId: UserWalletId,
val isUpgradeFlow: Boolean,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable
@ -353,6 +365,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}")
@Serializable
data class ForgetWallet(
val userWalletId: UserWalletId,
) : AppRoute(path = "/forget_wallet/${userWalletId.stringValue}")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,
@ -383,8 +400,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class TangemPayDetails(
val userWalletId: UserWalletId,
val config: TangemPayDetailsConfig,
) : AppRoute(path = "/tangem_pay_details")
) : AppRoute(path = "/tangem_pay_details/${userWalletId.stringValue}")
@Serializable
data class TangemPayOnboarding(

View file

@ -1,21 +1,30 @@
package com.tangem.common.ui.account
import com.tangem.common.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.quote.PriceChange
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
class AccountCryptoPortfolioItemStateConverter(
private val appCurrency: AppCurrency,
private val account: Account.CryptoPortfolio,
private val priceChangeLce: Lce<Unit, PriceChange>? = null,
private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null,
private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null,
) : Converter<TotalFiatBalance, TokenItemState> {
@ -31,6 +40,14 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content {
val subtitle2State = when (fiatBalance.amount.isZero()) {
true -> null
false -> priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
return TokenItemState.Content(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(this),
@ -50,14 +67,14 @@ class AccountCryptoPortfolioItemStateConverter(
.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
isFlickering = fiatBalance.source == StatusSource.CACHE,
),
subtitle2State = null,
subtitle2State = subtitle2State,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } },
)
}
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Loading {
return TokenItemState.Loading(
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
return TokenItemState.Content(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(account),
titleState = TokenItemState.TitleState.Content(
@ -71,6 +88,10 @@ class AccountCryptoPortfolioItemStateConverter(
),
isAvailable = false,
),
fiatAmountState = FiatAmountState.Loading,
subtitle2State = Subtitle2State.Loading,
onItemLongClick = null,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
)
}
@ -97,4 +118,14 @@ class AccountCryptoPortfolioItemStateConverter(
},
)
}
private fun BigDecimal.getPriceChangeType(): PriceChangeType = PriceChangeConverter.fromBigDecimal(value = this)
private fun StatusSource.isFlickering(): Boolean = this == StatusSource.CACHE
private fun PriceChange.toSubtitle2State(): Subtitle2State = Subtitle2State.PriceChangeContent(
priceChangePercent = this.value.format { percent() },
type = this.value.getPriceChangeType(),
isFlickering = this.source.isFlickering(),
)
}

View file

@ -33,7 +33,7 @@ sealed interface AccountNameUM {
*
* @property raw the raw string value of the custom account name
*/
class Custom(internal val raw: String) : AccountNameUM {
data class Custom(internal val raw: String) : AccountNameUM {
override val value: TextReference = stringReference(value = raw)
}

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
@ -29,6 +30,7 @@ fun AccountTitle(
accountTitleUM: AccountTitleUM,
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.subtitle2,
textColor: Color = TangemTheme.colors.text.tertiary,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -40,19 +42,20 @@ fun AccountTitle(
Text(
text = accountTitleUM.prefixText.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.tertiary,
color = textColor,
)
AccountLabel(
name = accountTitleUM.name,
icon = accountTitleUM.icon,
iconSize = AccountIconSize.ExtraSmall,
nameStyle = textStyle,
nameColor = textColor,
)
}
is AccountTitleUM.Text -> Text(
text = accountTitleUM.title.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.tertiary,
color = textColor,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
}

View file

@ -0,0 +1,119 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.AccountName
@Composable
fun PortfolioSelectRow(state: PortfolioSelectUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clickable(enabled = state.isMultiChoice, onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(leftText),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW12()
if (state.icon != null) {
AccountIcon(
name = state.name,
icon = state.icon,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = state.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (state.isMultiChoice) {
Icon(
modifier = Modifier
.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Immutable
data class PortfolioSelectUM(
val icon: CryptoPortfolioIconUM?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,
val onClick: () -> Unit,
)
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PortfolioSelectRowPreview(@PreviewParameter(PreviewProvider::class) state: PortfolioSelectUM) {
TangemThemePreview {
PortfolioSelectRow(
state = state,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
)
}
}
private class PreviewProvider : PreviewParameterProvider<PortfolioSelectUM> {
val account
get() = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
val wallet
get() = PortfolioSelectUM(
icon = null,
name = stringReference("Wallet Name"),
isMultiChoice = false,
isAccountMode = false,
onClick = {},
)
override val values: Sequence<PortfolioSelectUM>
get() = sequenceOf(account, wallet)
}

View file

@ -1,6 +1,7 @@
package com.tangem.core.analytics.utils
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
/**
[REDACTED_AUTHOR]
@ -9,9 +10,15 @@ interface AnalyticsContextProxy {
fun setContext(scanResponse: ScanResponse)
fun addContext(userWallet: UserWallet)
fun setHotWalletContext()
fun eraseContext()
fun addContext(scanResponse: ScanResponse)
fun addHotWalletContext()
fun removeContext()
}

View file

@ -30,9 +30,5 @@
{
"name": "zklink",
"version": "undefined"
},
{
"name": "scroll",
"version": "undefined"
}
]

View file

@ -1,5 +0,0 @@
package com.tangem.datasource.api.express.models
object TangemExpressValues {
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
}

View file

@ -20,4 +20,18 @@ data class GetWalletAccountsResponse(
@Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int,
)
}
/** Flattens the tokens from all wallet accounts into a single list */
fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
return accounts.flatMap { it.tokens.orEmpty() }
}
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
group = wallet.group,
sort = wallet.sort,
tokens = flattenTokens(),
)
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
}

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Express service loader
*
[REDACTED_AUTHOR]
*/
interface ExpressServiceLoader {
/** Update service using [userWallet] and [userTokens] */
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>
}

View file

@ -6,18 +6,21 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -34,6 +37,7 @@ data class SmallButtonConfig(
val onClick: () -> Unit,
val icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
val isEnabled: Boolean = true,
val isLoading: Boolean = false,
)
/**
@ -68,7 +72,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
label = "Update background color",
)
Row(
Box(
modifier = modifier
.defaultMinSize(
minWidth = TangemTheme.dimens.size46,
@ -79,7 +83,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
color = backgroundColor,
shape = shape,
)
.clickable(enabled = config.isEnabled, onClick = config.onClick)
.clickable(enabled = !config.isLoading && config.isEnabled, onClick = config.onClick)
.padding(
paddingValues = when (config.icon) {
is TangemButtonIconPosition.None -> PaddingValues(
@ -95,42 +99,54 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
)
},
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
contentAlignment = Alignment.Center,
) {
ContentContainer(
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = when {
!config.isEnabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
if (config.isLoading) {
CircularProgressIndicator(
strokeWidth = TangemTheme.dimens.size2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.size(TangemTheme.dimens.size16),
)
}
Row(
modifier = Modifier.conditional(config.isLoading) { alpha(0f) },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
ContentContainer(
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = when {
!config.isEnabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
Text(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
text = config.text.resolveReference(),
color = textColor,
maxLines = 1,
style = TangemTheme.typography.button,
)
},
icon = { iconResId ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId),
tint = if (config.isEnabled) {
TangemTheme.colors.icon.secondary
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
},
)
Text(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
text = config.text.resolveReference(),
color = textColor,
maxLines = 1,
style = TangemTheme.typography.button,
)
},
icon = { iconResId ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId),
tint = if (config.isEnabled) {
TangemTheme.colors.icon.secondary
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
},
)
}
}
}
@ -172,6 +188,7 @@ private fun ButtonsSample() {
)
PrimarySmallButton(config = config)
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add")))
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"), isLoading = true))
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Rating"),
@ -191,5 +208,12 @@ private fun ButtonsSample() {
isEnabled = false,
),
)
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Add token"),
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
isLoading = true,
),
)
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.core.ui.components.feature
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewFeatureBlock() {
TangemThemePreview {
Column(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(16.dp),
) {
FeatureBlock(
title = stringResourceSafe(R.string.backup_info_save_title),
description = stringResourceSafe(R.string.backup_info_save_description, "12"),
iconRes = R.drawable.ic_lock_24,
)
Spacer(modifier = Modifier.height(24.dp))
FeatureBlock(
title = stringResourceSafe(R.string.backup_info_keep_title),
description = stringResourceSafe(R.string.backup_info_keep_description),
iconRes = R.drawable.ic_settings_24,
)
}
}
}

View file

@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
* @param onImageError composable to show if image loading failed
*/
@Composable
internal fun InputRowAsyncImage(
fun InputRowAsyncImage(
imageUrl: String,
modifier: Modifier = Modifier,
isGrayscale: Boolean = false,

View file

@ -53,7 +53,7 @@ fun SelectorRowItem(
val textStyle = if (isSelected && showSelectedAppearance) {
TangemTheme.typography.subtitle2
} else {
TangemTheme.typography.body2
TangemTheme.typography.body1
}
Box(
modifier = modifier

View file

@ -27,6 +27,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference
@ -691,7 +692,10 @@ object AccountItemPreviewData {
value = stringReference("24 tokens"),
isAvailable = false,
),
subtitle2State = null,
subtitle2State = Subtitle2State.PriceChangeContent(
priceChangePercent = "0,43 %",
type = PriceChangeType.UP,
),
onItemClick = {},
onItemLongClick = {},
)

View file

@ -11,7 +11,6 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -31,7 +30,7 @@ internal fun TokenCryptoAmount(
isFlickering = state.isFlickering,
)
}
is TokenItemState.Subtitle2State.LabelContent -> {
is TokenCryptoAmountState.LabelContent -> {
AuditLabel(state = state.auditLabelUM, modifier = modifier)
}
is TokenCryptoAmountState.Unreachable -> {
@ -46,6 +45,15 @@ internal fun TokenCryptoAmount(
is TokenCryptoAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
is TokenCryptoAmountState.PriceChangeContent -> {
PriceBlock(
modifier = modifier,
price = null,
type = state.type,
priceChangePercent = state.priceChangePercent,
isFlickering = state.isFlickering,
)
}
null -> Unit
}
}

View file

@ -64,8 +64,8 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier)
}
@Composable
private fun PriceBlock(
price: String,
internal fun PriceBlock(
price: String?,
isFlickering: Boolean,
modifier: Modifier = Modifier,
type: PriceChangeType? = null,
@ -75,13 +75,15 @@ private fun PriceBlock(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
PriceText(
modifier = Modifier.weight(weight = 1f, fill = false),
text = price,
isFlickering = isFlickering,
)
if (price != null) {
PriceText(
modifier = Modifier.weight(weight = 1f, fill = false),
text = price,
isFlickering = isFlickering,
)
SpacerW6()
SpacerW6()
}
if (type != null) {
PriceChangeIcon(

View file

@ -230,6 +230,12 @@ sealed class TokenItemState {
val isFlickering: Boolean = false,
) : Subtitle2State()
data class PriceChangeContent(
val priceChangePercent: String,
val type: PriceChangeType,
val isFlickering: Boolean = false,
) : Subtitle2State()
data class LabelContent(val auditLabelUM: AuditLabelUM) : Subtitle2State()
data object Unreachable : Subtitle2State()

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
/**
* Utility class for keeping themed color reference from app theme.
*
* It necessary to use [Immutable] annotation for runtime stability.
*
* @property value color provider from theme
*/
@Immutable
data class ColorReference(val value: @Composable () -> Color)
/**
* Creates a [ColorReference] using a themed color from the app theme with a lambda.
*
* @param value The color provider from theme.
* @return A [ColorReference] representing the themed color.
*/
fun themedColor(value: @Composable () -> Color): ColorReference {
return ColorReference(value)
}
/**
* Resolves [ColorReference] to [Color]
*/
@Composable
fun ColorReference.resolveReference(): Color {
return value()
}

View file

@ -20,6 +20,7 @@ object TangemColorPalette {
// region Light
val Light1 = Color(0xFFF5F5F5)
val Light1V2 = Color(0xFFF4F4F4)
val Light2 = Color(0xFFEBEBEB)
val Light3 = Color(0xFFD3D3D3)
val Light4 = Color(0xFFC9C9C9)
@ -27,6 +28,7 @@ object TangemColorPalette {
// endregion Light
// region Green
val Green = Color(0xFF0C9F3D)
val Meadow = Color(0xFF1ACE80)
val MagicMint = Color(0xFFA3EBCC)
val DarkGreen = Color(0xFF06311F)
@ -45,4 +47,9 @@ object TangemColorPalette {
val Tangerine = Color(0xFFFFB71B)
val Mustard = Color(0xFFFDDE55)
// endregion Yellow
// region Overlay
val Overlay1 = Color(0x66000000)
val Overlay2 = Color(0xB2000000)
// endregion Overlay
}

View file

@ -0,0 +1,536 @@
@file:Suppress("LongParameterList")
package com.tangem.core.ui.res
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
@Stable
class TangemColors2 internal constructor(
val text: Text,
val graphic: Graphic,
val button: Button,
val surface: Surface,
val controls: Controls,
val field: Field,
val overlay: Overlay,
val border: Border,
val fill: Fill,
val skeleton: Skeleton,
val markers: Markers,
) {
@Stable
class Text internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
primaryInverted: Color,
secondary: Color,
tertiary: Color,
primaryInvertedConstant: Color,
) {
var primary by mutableStateOf(primary)
private set
var primaryInverted by mutableStateOf(primaryInverted)
private set
var secondary by mutableStateOf(secondary)
private set
var tertiary by mutableStateOf(tertiary)
private set
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
private set
fun update(other: Neutral) {
primary = other.primary
primaryInverted = other.primaryInverted
secondary = other.secondary
tertiary = other.tertiary
primaryInvertedConstant = other.primaryInvertedConstant
}
}
@Stable
class Status internal constructor(
disabled: Color,
accent: Color,
warning: Color,
attention: Color,
positive: Color,
) {
var disabled by mutableStateOf(disabled)
private set
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
var positive by mutableStateOf(positive)
private set
fun update(other: Status) {
disabled = other.disabled
accent = other.accent
warning = other.warning
attention = other.attention
positive = other.positive
}
}
fun update(other: Text) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Graphic internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
primaryInverted: Color,
secondary: Color,
tertiary: Color,
quaternary: Color,
primaryInvertedConstant: Color,
tertiaryConstant: Color,
) {
var primary by mutableStateOf(primary)
private set
var primaryInverted by mutableStateOf(primaryInverted)
private set
var secondary by mutableStateOf(secondary)
private set
var tertiary by mutableStateOf(tertiary)
private set
var quaternary by mutableStateOf(quaternary)
private set
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
private set
var tertiaryConstant by mutableStateOf(tertiaryConstant)
private set
fun update(other: Neutral) {
primary = other.primary
primaryInverted = other.primaryInverted
secondary = other.secondary
tertiary = other.tertiary
quaternary = other.quaternary
primaryInvertedConstant = other.primaryInvertedConstant
tertiaryConstant = other.tertiaryConstant
}
}
@Stable
class Status internal constructor(
accent: Color,
warning: Color,
attention: Color,
) {
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
fun update(other: Status) {
accent = other.accent
warning = other.warning
attention = other.attention
}
}
fun update(other: Graphic) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Button internal constructor(
backgroundPrimary: Color,
backgroundSecondary: Color,
backgroundDisabled: Color,
backgroundPositive: Color,
textPrimary: Color,
textSecondary: Color,
textDisabled: Color,
iconPrimary: Color,
iconSecondary: Color,
iconDisabled: Color,
borderPrimary: Color,
) {
var backgroundPrimary by mutableStateOf(backgroundPrimary)
private set
var backgroundSecondary by mutableStateOf(backgroundSecondary)
private set
var backgroundDisabled by mutableStateOf(backgroundDisabled)
private set
var backgroundPositive by mutableStateOf(backgroundPositive)
private set
var textPrimary by mutableStateOf(textPrimary)
private set
var textSecondary by mutableStateOf(textSecondary)
private set
var textDisabled by mutableStateOf(textDisabled)
private set
var iconPrimary by mutableStateOf(iconPrimary)
private set
var iconSecondary by mutableStateOf(iconSecondary)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
var borderPrimary by mutableStateOf(borderPrimary)
private set
fun update(other: Button) {
backgroundPrimary = other.backgroundPrimary
backgroundSecondary = other.backgroundSecondary
backgroundDisabled = other.backgroundDisabled
backgroundPositive = other.backgroundPositive
textPrimary = other.textPrimary
textSecondary = other.textSecondary
textDisabled = other.textDisabled
iconPrimary = other.iconPrimary
iconSecondary = other.iconSecondary
iconDisabled = other.iconDisabled
borderPrimary = other.borderPrimary
}
}
@Stable
class Surface internal constructor(
level1: Color,
level2: Color,
level3: Color,
level4: Color,
) {
var level1 by mutableStateOf(level1)
private set
var level2 by mutableStateOf(level2)
private set
var level3 by mutableStateOf(level3)
private set
var level4 by mutableStateOf(level4)
private set
fun update(other: Surface) {
level1 = other.level1
level2 = other.level2
level3 = other.level3
level4 = other.level4
}
}
@Stable
class Controls internal constructor(
backgroundDefault: Color,
backgroundChecked: Color,
iconDefault: Color,
iconDisabled: Color,
) {
var backgroundDefault by mutableStateOf(backgroundDefault)
private set
var backgroundChecked by mutableStateOf(backgroundChecked)
private set
var iconDefault by mutableStateOf(iconDefault)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
fun update(other: Controls) {
backgroundDefault = other.backgroundDefault
backgroundChecked = other.backgroundChecked
iconDefault = other.iconDefault
iconDisabled = other.iconDisabled
}
}
@Stable
class Field internal constructor(
backgroundDefault: Color,
backgroundFocused: Color,
textPlaceholder: Color,
textDefault: Color,
textDisabled: Color,
iconDefault: Color,
iconDisabled: Color,
textInvalid: Color,
borderInvalid: Color,
) {
var backgroundDefault by mutableStateOf(backgroundDefault)
private set
var backgroundFocused by mutableStateOf(backgroundFocused)
private set
var textPlaceholder by mutableStateOf(textPlaceholder)
private set
var textDefault by mutableStateOf(textDefault)
private set
var textDisabled by mutableStateOf(textDisabled)
private set
var iconDefault by mutableStateOf(iconDefault)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
var textInvalid by mutableStateOf(textInvalid)
private set
var borderInvalid by mutableStateOf(borderInvalid)
private set
fun update(other: Field) {
backgroundDefault = other.backgroundDefault
backgroundFocused = other.backgroundFocused
textPlaceholder = other.textPlaceholder
textDefault = other.textDefault
textDisabled = other.textDisabled
iconDefault = other.iconDefault
iconDisabled = other.iconDisabled
textInvalid = other.textInvalid
borderInvalid = other.borderInvalid
}
}
@Stable
class Overlay internal constructor(
overlayPrimary: Color,
overlaySecondary: Color,
) {
var overlayPrimary by mutableStateOf(overlayPrimary)
private set
var overlaySecondary by mutableStateOf(overlaySecondary)
private set
fun update(other: Overlay) {
overlayPrimary = other.overlayPrimary
overlaySecondary = other.overlaySecondary
}
}
@Stable
class Border internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
secondary: Color,
) {
var primary by mutableStateOf(primary)
private set
var secondary by mutableStateOf(secondary)
private set
fun update(other: Neutral) {
primary = other.primary
secondary = other.secondary
}
}
@Stable
class Status internal constructor(
accent: Color,
warning: Color,
attention: Color,
) {
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
fun update(other: Status) {
accent = other.accent
warning = other.warning
attention = other.attention
}
}
fun update(other: Border) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Fill internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
primaryInverted: Color,
primaryInvertedConstant: Color,
secondary: Color,
tertiaryConstant: Color,
quaternary: Color,
) {
var primary by mutableStateOf(primary)
private set
var primaryInverted by mutableStateOf(primaryInverted)
private set
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
private set
var secondary by mutableStateOf(secondary)
private set
var tertiaryConstant by mutableStateOf(tertiaryConstant)
private set
var quaternary by mutableStateOf(quaternary)
private set
fun update(other: Neutral) {
primary = other.primary
primaryInverted = other.primaryInverted
primaryInvertedConstant = other.primaryInvertedConstant
secondary = other.secondary
tertiaryConstant = other.tertiaryConstant
quaternary = other.quaternary
}
}
@Stable
class Status internal constructor(
accent: Color,
warning: Color,
attention: Color,
) {
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
fun update(other: Status) {
accent = other.accent
warning = other.warning
attention = other.attention
}
}
fun update(other: Fill) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Skeleton internal constructor(
backgroundPrimary: Color,
) {
var backgroundPrimary by mutableStateOf(backgroundPrimary)
private set
fun update(other: Skeleton) {
backgroundPrimary = other.backgroundPrimary
}
}
@Stable
class Markers internal constructor(
backgroundSolidGray: Color,
backgroundDisabled: Color,
backgroundSolidBlue: Color,
textGray: Color,
textDisabled: Color,
iconGray: Color,
iconDisabled: Color,
borderGray: Color,
backgroundTintedBlue: Color,
textBlue: Color,
backgroundSolidRed: Color,
backgroundTintedRed: Color,
iconBlue: Color,
iconRed: Color,
textRed: Color,
backgroundTintedGray: Color,
borderTintedBlue: Color,
borderTintedRed: Color,
) {
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
private set
var backgroundDisabled by mutableStateOf(backgroundDisabled)
private set
var backgroundSolidBlue by mutableStateOf(backgroundSolidBlue)
private set
var textGray by mutableStateOf(textGray)
private set
var textDisabled by mutableStateOf(textDisabled)
private set
var iconGray by mutableStateOf(iconGray)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
var borderGray by mutableStateOf(borderGray)
private set
var backgroundTintedBlue by mutableStateOf(backgroundTintedBlue)
private set
var textBlue by mutableStateOf(textBlue)
private set
var backgroundSolidRed by mutableStateOf(backgroundSolidRed)
private set
var backgroundTintedRed by mutableStateOf(backgroundTintedRed)
private set
var iconBlue by mutableStateOf(iconBlue)
private set
var iconRed by mutableStateOf(iconRed)
private set
var textRed by mutableStateOf(textRed)
private set
var backgroundTintedGray by mutableStateOf(backgroundTintedGray)
private set
var borderTintedBlue by mutableStateOf(borderTintedBlue)
private set
var borderTintedRed by mutableStateOf(borderTintedRed)
private set
fun update(other: Markers) {
backgroundSolidGray = other.backgroundSolidGray
backgroundDisabled = other.backgroundDisabled
backgroundSolidBlue = other.backgroundSolidBlue
textGray = other.textGray
textDisabled = other.textDisabled
iconGray = other.iconGray
iconDisabled = other.iconDisabled
borderGray = other.borderGray
backgroundTintedBlue = other.backgroundTintedBlue
textBlue = other.textBlue
backgroundSolidRed = other.backgroundSolidRed
backgroundTintedRed = other.backgroundTintedRed
iconBlue = other.iconBlue
iconRed = other.iconRed
textRed = other.textRed
backgroundTintedGray = other.backgroundTintedGray
borderTintedBlue = other.borderTintedBlue
borderTintedRed = other.borderTintedRed
}
}
fun update(other: TangemColors2) {
text.update(other.text)
graphic.update(other.graphic)
button.update(other.button)
surface.update(other.surface)
controls.update(other.controls)
field.update(other.field)
overlay.update(other.overlay)
border.update(other.border)
fill.update(other.fill)
skeleton.update(other.skeleton)
markers.update(other.markers)
}
}

View file

@ -138,6 +138,11 @@ object TangemTheme {
@ReadOnlyComposable
get() = LocalTangemColors.current
val colors2: TangemColors2
@Composable
@ReadOnlyComposable
get() = LocalTangemColors2.current
val typography: TangemTypography
@Composable
@ReadOnlyComposable
@ -156,7 +161,7 @@ object TangemTheme {
@Stable
@Composable
private fun tangemColorScheme(colors: TangemColors): ColorScheme {
internal fun tangemColorScheme(colors: TangemColors): ColorScheme {
return ColorScheme(
primary = colors.background.primary,
onPrimary = colors.text.primary1,
@ -206,7 +211,7 @@ private fun tangemColorScheme(colors: TangemColors): ColorScheme {
@Composable
@ReadOnlyComposable
private fun lightThemeColors(): TangemColors {
internal fun lightThemeColors(redesign: Boolean = false): TangemColors {
return TangemColors(
text = TangemColors.Text(
primary1 = TangemColorPalette.Dark6,
@ -233,8 +238,8 @@ private fun lightThemeColors(): TangemColors {
),
background = TangemColors.Background(
primary = TangemColorPalette.White,
secondary = TangemColorPalette.Light1,
tertiary = TangemColorPalette.Light1,
secondary = if (redesign) TangemColorPalette.Light1V2 else TangemColorPalette.Light1,
tertiary = if (redesign) TangemColorPalette.Light1V2 else TangemColorPalette.Light1,
action = TangemColorPalette.White,
),
control = TangemColors.Control(
@ -248,7 +253,7 @@ private fun lightThemeColors(): TangemColors {
transparency = TangemColorPalette.White,
),
field = TangemColors.Field(
primary = TangemColorPalette.Light1,
primary = if (redesign) TangemColorPalette.Light1V2 else TangemColorPalette.Light1,
focused = TangemColorPalette.Light2,
),
overlay = TangemColors.Overlay(
@ -260,7 +265,7 @@ private fun lightThemeColors(): TangemColors {
@Composable
@ReadOnlyComposable
private fun darkThemeColors(): TangemColors {
internal fun darkThemeColors(): TangemColors {
return TangemColors(
text = TangemColors.Text(
primary1 = TangemColorPalette.White,
@ -320,12 +325,16 @@ private val TangemTextSelectionColors: TextSelectionColors
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
internal val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
error("No TangemColors provided")
}
private val LocalTangemTypography = staticCompositionLocalOf {
TangemTypography()
internal val LocalTangemColors2 = staticCompositionLocalOf<TangemColors2> {
error("No TangemColors2 provided")
}
internal val LocalTangemTypography = staticCompositionLocalOf {
TangemTypography(RobotoFamily)
}
private val LocalTangemDimens = staticCompositionLocalOf {

View file

@ -0,0 +1,309 @@
@file:Suppress("LongMethod")
package com.tangem.core.ui.res
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
/**
* Provides additional theming for redesigned components.
* Used together with [TangemTheme].
* @param content Composable content where the theme is applied.
*/
@Composable
fun TangemThemeRedesign(content: @Composable () -> Unit) {
val themeColors = if (LocalIsInDarkTheme.current) darkThemeColors() else lightThemeColors(redesign = true)
val rememberedColors = remember { themeColors }
.also { it.update(themeColors) }
val rootBackgroundColor = rememberedColors.background.secondary
MaterialTheme(
colorScheme = tangemColorScheme(colors = themeColors),
) {
CompositionLocalProvider(
LocalTangemColors provides themeColors,
LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(),
LocalTangemTypography provides TangemTypography(InterFamily),
LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) },
) {
content()
}
}
}
@Composable
@ReadOnlyComposable
private fun lightThemeColors2(): TangemColors2 {
val text = TangemColors2.Text(
neutral = TangemColors2.Text.Neutral(
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
secondary = TangemColorPalette.Dark2,
tertiary = TangemColorPalette.Dark3,
primaryInvertedConstant = TangemColorPalette.White,
),
status = TangemColors2.Text.Status(
disabled = TangemColorPalette.Light4,
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
positive = TangemColorPalette.Green,
),
)
val graphic = TangemColors2.Graphic(
neutral = TangemColors2.Graphic.Neutral(
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
secondary = TangemColorPalette.Dark2,
tertiary = TangemColorPalette.Dark3,
quaternary = TangemColorPalette.Light4,
primaryInvertedConstant = TangemColorPalette.White,
tertiaryConstant = TangemColorPalette.Dark3,
),
status = TangemColors2.Graphic.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
)
val border = TangemColors2.Border(
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Light3,
secondary = TangemColorPalette.Light5,
),
status = TangemColors2.Border.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
)
val overlay = TangemColors2.Overlay(
overlayPrimary = TangemColorPalette.Overlay1,
overlaySecondary = TangemColorPalette.Overlay2,
)
val fill = TangemColors2.Fill(
neutral = TangemColors2.Fill.Neutral(
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
primaryInvertedConstant = TangemColorPalette.White,
secondary = TangemColorPalette.Dark3,
tertiaryConstant = TangemColorPalette.Dark3,
quaternary = TangemColorPalette.Light4,
),
status = TangemColors2.Fill.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
)
val button = TangemColors2.Button(
backgroundPrimary = TangemColorPalette.Dark6,
backgroundSecondary = TangemColorPalette.Dark6.copy(alpha = 0.1f),
backgroundDisabled = TangemColorPalette.Light3,
backgroundPositive = TangemColorPalette.Azure,
textSecondary = TangemColorPalette.Dark6,
textPrimary = TangemColorPalette.Light2,
textDisabled = text.neutral.tertiary,
iconPrimary = TangemColorPalette.Dark6,
iconSecondary = TangemColorPalette.Light1V2,
iconDisabled = TangemColorPalette.Light2,
borderPrimary = TangemColorPalette.Dark6,
)
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.White,
level2 = TangemColorPalette.Light1V2,
level3 = TangemColorPalette.Light1V2,
level4 = TangemColorPalette.White,
)
val controls = TangemColors2.Controls(
backgroundChecked = TangemColorPalette.Dark6,
backgroundDefault = TangemColorPalette.Light2,
iconDefault = TangemColorPalette.White,
iconDisabled = TangemColorPalette.White,
)
val field = TangemColors2.Field(
backgroundDefault = TangemColorPalette.Light1V2,
backgroundFocused = TangemColorPalette.Light3,
textPlaceholder = text.neutral.secondary,
textDefault = text.neutral.primary,
textDisabled = text.neutral.tertiary,
iconDefault = graphic.neutral.tertiary,
iconDisabled = graphic.neutral.quaternary,
textInvalid = text.status.warning,
borderInvalid = border.status.warning,
)
val skeleton = TangemColors2.Skeleton(
backgroundPrimary = TangemColorPalette.Light1V2,
)
val markers = TangemColors2.Markers(
backgroundSolidGray = TangemColorPalette.Light3,
backgroundDisabled = TangemColorPalette.Light3,
backgroundSolidBlue = TangemColorPalette.Azure,
textGray = TangemColorPalette.Dark2,
textDisabled = text.neutral.tertiary,
iconGray = TangemColorPalette.Dark1,
iconDisabled = TangemColorPalette.Light2,
borderGray = TangemColorPalette.Light3,
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
iconBlue = TangemColorPalette.Azure,
iconRed = TangemColorPalette.Amaranth,
textRed = TangemColorPalette.Amaranth,
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
return TangemColors2(
text = text,
graphic = graphic,
border = border,
overlay = overlay,
fill = fill,
button = button,
surface = surface,
controls = controls,
field = field,
skeleton = skeleton,
markers = markers,
)
}
@Composable
@ReadOnlyComposable
private fun darkThemeColors2(): TangemColors2 {
val text = TangemColors2.Text(
neutral = TangemColors2.Text.Neutral(
primary = TangemColorPalette.White,
primaryInverted = TangemColorPalette.Dark6,
secondary = TangemColorPalette.Light5,
tertiary = TangemColorPalette.Dark1,
primaryInvertedConstant = TangemColorPalette.White,
),
status = TangemColors2.Text.Status(
disabled = TangemColorPalette.Dark3,
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
positive = TangemColorPalette.Green,
),
)
val graphic = TangemColors2.Graphic(
neutral = TangemColors2.Graphic.Neutral(
primary = TangemColorPalette.White,
primaryInverted = TangemColorPalette.Dark6,
secondary = TangemColorPalette.Light5,
tertiary = TangemColorPalette.Dark3,
quaternary = TangemColorPalette.Dark3,
tertiaryConstant = TangemColorPalette.Dark3,
primaryInvertedConstant = TangemColorPalette.White,
),
status = TangemColors2.Graphic.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
)
val border = TangemColors2.Border(
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Dark4,
secondary = TangemColorPalette.Dark4,
),
status = TangemColors2.Border.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
)
val overlay = TangemColors2.Overlay(
overlayPrimary = TangemColorPalette.Overlay1,
overlaySecondary = TangemColorPalette.Overlay2,
)
val fill = TangemColors2.Fill(
neutral = TangemColors2.Fill.Neutral(
primary = TangemColorPalette.White,
primaryInverted = TangemColorPalette.Dark6,
primaryInvertedConstant = TangemColorPalette.White,
secondary = TangemColorPalette.Light5,
tertiaryConstant = TangemColorPalette.Dark1,
quaternary = TangemColorPalette.Dark3,
),
status = TangemColors2.Fill.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
)
val button = TangemColors2.Button(
backgroundPrimary = TangemColorPalette.Light1V2,
backgroundSecondary = TangemColorPalette.White.copy(alpha = 0.1f),
backgroundDisabled = TangemColorPalette.Dark5,
backgroundPositive = TangemColorPalette.Azure,
textSecondary = TangemColorPalette.Light4,
textPrimary = TangemColorPalette.Dark4,
textDisabled = text.neutral.secondary,
iconPrimary = TangemColorPalette.Light4,
iconSecondary = TangemColorPalette.Dark4,
iconDisabled = TangemColorPalette.Dark5,
borderPrimary = TangemColorPalette.Light4,
)
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.Dark6,
level2 = TangemColorPalette.Black,
level3 = TangemColorPalette.Dark6,
level4 = TangemColorPalette.Dark5,
)
val controls = TangemColors2.Controls(
backgroundChecked = TangemColorPalette.Azure,
backgroundDefault = TangemColorPalette.Dark4,
iconDefault = TangemColorPalette.White,
iconDisabled = TangemColorPalette.White,
)
val field = TangemColors2.Field(
backgroundDefault = TangemColorPalette.Dark6,
backgroundFocused = TangemColorPalette.Dark4,
textPlaceholder = text.neutral.secondary,
textDefault = text.neutral.primary,
textDisabled = text.neutral.tertiary,
iconDefault = graphic.neutral.tertiary,
iconDisabled = graphic.neutral.quaternary,
textInvalid = text.status.warning,
borderInvalid = border.status.warning,
)
val skeleton = TangemColors2.Skeleton(
backgroundPrimary = TangemColorPalette.Dark5,
)
val markers = TangemColors2.Markers(
backgroundSolidGray = TangemColorPalette.Dark5,
backgroundDisabled = TangemColorPalette.Dark5,
backgroundSolidBlue = TangemColorPalette.Azure,
textGray = TangemColorPalette.Light4,
textDisabled = text.neutral.secondary,
iconGray = TangemColorPalette.Dark2,
iconDisabled = TangemColorPalette.Dark5,
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
iconBlue = TangemColorPalette.Azure,
iconRed = TangemColorPalette.Flamingo,
textRed = TangemColorPalette.Flamingo,
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
return TangemColors2(
text = text,
graphic = graphic,
border = border,
overlay = overlay,
fill = fill,
button = button,
surface = surface,
controls = controls,
field = field,
skeleton = skeleton,
markers = markers,
)
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.TextUnit
@ -11,15 +12,22 @@ import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.R
private val RobotoFamily = FontFamily(
internal val RobotoFamily = FontFamily(
Font(R.font.roboto_regular, FontWeight.Normal),
Font(R.font.roboto_medium, FontWeight.Medium),
)
internal val InterFamily = FontFamily(
Font(R.font.inter_regular),
Font(R.font.inter_italic, style = FontStyle.Italic),
)
@Immutable
data class TangemTypography internal constructor(
class TangemTypography internal constructor(
fontFamily: FontFamily,
) {
val head: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 34.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
@ -28,9 +36,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val h1: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 34.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
@ -39,9 +47,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val h2: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.18f, type = TextUnitType.Sp),
@ -50,9 +58,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val h3: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 20.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp),
@ -61,9 +69,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val subtitle1: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp),
@ -72,9 +80,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val subtitle2: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
@ -83,9 +91,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val body1: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp),
@ -94,9 +102,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val body2: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.25f, type = TextUnitType.Sp),
@ -105,9 +113,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val button: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
@ -116,9 +124,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val caption1: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp),
@ -127,9 +135,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val caption2: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp),
@ -138,9 +146,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
val overline: TextStyle = TextStyle(
fontFamily = RobotoFamily,
fontFamily = fontFamily,
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 1.5f, type = TextUnitType.Sp),
@ -149,5 +157,5 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
),
)
)
}

View file

@ -1,6 +1,6 @@
package com.tangem.core.ui.test
object BaseAmountBlockTestTags {
const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT"
const val PRIMARY_AMOUNT = "SEND_DETAILS_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "SEND_DETAILS_SCREEN_SECONDARY_AMOUNT"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M69.781,45.692L69.79,45.709C71.126,48.06 72,50.879 72,53.877C72,57.927 70.612,62.075 67.529,65.265C64.394,68.508 60.145,70.096 55.73,70.096H16.27C11.855,70.096 7.606,68.508 4.471,65.265C1.388,62.075 0,57.927 0,53.877C0,51.094 0.713,48.238 2.266,45.611L21.966,11.287C25.02,5.881 30.514,3.086 36,3.086C41.553,3.086 46.97,5.934 50.018,11.26C50.02,11.263 50.022,11.266 50.023,11.269L69.781,45.692ZM41.932,15.893L61.688,50.313C62.324,51.433 62.68,52.681 62.68,53.877C62.68,57.772 60.058,60.777 55.73,60.777H16.27C11.942,60.777 9.32,57.772 9.32,53.877C9.32,52.681 9.625,51.459 10.312,50.313L30.068,15.893C31.367,13.577 33.658,12.406 36,12.406C38.342,12.406 40.608,13.577 41.932,15.893Z"
android:strokeAlpha="0.12"
android:fillColor="#FF3333"
android:fillType="evenOdd"
android:fillAlpha="0.12"/>
<path
android:pathData="M36,9.771C39.182,9.771 42.273,11.382 44.047,14.484L44.049,14.486L63.49,48.36C64.308,49.799 64.8,51.456 64.8,53.126C64.8,55.617 63.955,57.976 62.262,59.728C60.554,61.494 58.159,62.462 55.414,62.462H16.586C13.842,62.462 11.446,61.494 9.738,59.728C8.045,57.976 7.2,55.617 7.2,53.126C7.2,51.514 7.613,49.864 8.526,48.331L27.948,14.493C29.705,11.368 32.836,9.771 36,9.771ZM36,12.318C33.695,12.318 31.441,13.471 30.163,15.751L10.725,49.618C10.048,50.745 9.747,51.949 9.747,53.126C9.747,56.958 12.328,59.914 16.586,59.914H55.414C59.673,59.914 62.253,56.958 62.253,53.126C62.253,51.949 61.902,50.72 61.275,49.618L41.837,15.751C40.534,13.471 38.305,12.318 36,12.318ZM34.984,45.657C35.942,45.657 36.718,46.433 36.718,47.391C36.718,48.348 35.942,49.124 34.984,49.124C34.027,49.124 33.251,48.348 33.251,47.391C33.251,46.433 34.027,45.657 34.984,45.657ZM34.984,25.845C35.805,25.845 36.471,26.51 36.471,27.331V42.189C36.471,43.01 35.805,43.676 34.984,43.676C34.164,43.676 33.499,43.01 33.499,42.189V27.331C33.499,26.511 34.164,25.845 34.984,25.845Z"
android:fillColor="#FF3333"/>
</vector>

Binary file not shown.

Binary file not shown.

View file

@ -28,5 +28,4 @@ class TestingCoroutineDispatcherProvider(
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
override val default: CoroutineDispatcher = Dispatchers.Unconfined,
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
) : CoroutineDispatcherProvider
) : CoroutineDispatcherProvider

View file

@ -27,6 +27,7 @@ dependencies {
api(projects.domain.common)
api(projects.domain.models)
api(projects.domain.tokens)
api(projects.domain.wallets)
// endregion
// region Project - Data

View file

@ -36,6 +36,7 @@ internal class CryptoPortfolioConverter @AssistedInject constructor(
responseCryptoCurrenciesFactory.createCurrencies(
tokens = tokens,
userWallet = userWallet,
accountIndex = value.derivationIndex.toDerivationIndex(),
).toSet()
} else {
emptySet()

View file

@ -10,9 +10,9 @@ import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.repository.AccountsCRUDRepository
@ -42,7 +42,6 @@ internal object AccountDataModule {
accountsResponseStoreFactory: AccountsResponseStoreFactory,
userWalletsStore: UserWalletsStore,
userTokensSaver: UserTokensSaver,
eTagsStore: ETagsStore,
accountConverterFactoryContainer: AccountConverterFactoryContainer,
dispatchers: CoroutineDispatcherProvider,
): AccountsCRUDRepository {
@ -53,7 +52,7 @@ internal object AccountDataModule {
archivedAccountsStoreFactory = ArchivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
archivedAccountsETagStore = RuntimeStateStore(emptyMap()),
convertersContainer = accountConverterFactoryContainer,
dispatchers = dispatchers,
)

View file

@ -0,0 +1,18 @@
package com.tangem.data.account.di
import com.tangem.data.account.producer.DefaultSingleAccountProducer
import com.tangem.domain.account.producer.SingleAccountProducer
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface SingleAccountProducerFactoryModule {
@Binds
@Singleton
fun bindSingleAccountProducerFactory(impl: DefaultSingleAccountProducer.Factory): SingleAccountProducer.Factory
}

View file

@ -0,0 +1,23 @@
package com.tangem.data.account.di
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SingleAccountSupplierModule {
@Provides
@Singleton
fun provideSingleAccountSupplier(factory: SingleAccountProducer.Factory): SingleAccountSupplier {
return object : SingleAccountSupplier(
factory = factory,
keyCreator = { "single_account_${it.accountId.value}" },
) {}
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.api.safeApiCall
@ -19,6 +18,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -51,18 +51,25 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : WalletAccountsFetcher, WalletAccountsSaver {
override suspend fun fetch(userWalletId: UserWalletId) {
override suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse {
val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
val accountsResponse = fetchWalletAccounts(userWalletId, savedAccountsResponse)
?: return
if (accountsResponse.accounts.isEmpty()) {
initializeAccounts(userWalletId, accountsResponse)
} else if (accountsResponse.unassignedTokens.isNotEmpty()) {
assignTokens(userWalletId, accountsResponse)
return when {
accountsResponse.accounts.isEmpty() -> {
initializeAccounts(userWalletId, accountsResponse)
}
accountsResponse.unassignedTokens.isNotEmpty() -> {
assignTokens(userWalletId, accountsResponse)
}
else -> accountsResponse
}
}
override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? {
return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
}
override suspend fun store(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
val store = getAccountsResponseStore(userWalletId = userWalletId)
@ -115,7 +122,7 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
private suspend fun fetchWalletAccounts(
userWalletId: UserWalletId,
savedAccountsResponse: GetWalletAccountsResponse?,
): GetWalletAccountsResponse? {
): GetWalletAccountsResponse {
return safeApiCall(
call = {
val apiResponse = withContext(dispatchers.io) {
@ -145,7 +152,10 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
)
}
private suspend fun initializeAccounts(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
private suspend fun initializeAccounts(
userWalletId: UserWalletId,
accountsResponse: GetWalletAccountsResponse,
): GetWalletAccountsResponse {
val response = defaultWalletAccountsResponseFactory.create(
userWalletId = userWalletId,
userTokensResponse = UserTokensResponse(
@ -156,14 +166,17 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
)
userTokensSaver.push(userWalletId = userWalletId, response = response.toUserTokensResponse())
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts)
val syncedResponse = push(userWalletId = userWalletId, accounts = response.accounts) ?: response
if (syncedResponse != null) {
store(userWalletId = userWalletId, response = syncedResponse)
}
store(userWalletId = userWalletId, response = syncedResponse)
return syncedResponse
}
private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) {
private suspend fun assignTokens(
userWalletId: UserWalletId,
accountsResponse: GetWalletAccountsResponse,
): GetWalletAccountsResponse {
val accountsResponseWithTokens = accountsResponse.assignTokens(userWalletId)
store(userWalletId = userWalletId, response = accountsResponseWithTokens)
@ -172,6 +185,8 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
userWalletId = userWalletId,
response = accountsResponseWithTokens.toUserTokensResponse(),
)
return accountsResponseWithTokens
}
private suspend fun getETag(userWalletId: UserWalletId): String? {

View file

@ -1,7 +1,6 @@
package com.tangem.data.account.fetcher
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponseError
@ -10,6 +9,7 @@ import com.tangem.datasource.api.common.response.isNetworkError
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import timber.log.Timber
@ -49,11 +49,13 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
savedAccountsResponse: GetWalletAccountsResponse?,
pushWalletAccounts: suspend (UserWalletId, List<WalletAccountDTO>) -> GetWalletAccountsResponse?,
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
): GetWalletAccountsResponse? {
): GetWalletAccountsResponse {
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
if (isResponseUpToDate) {
Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
return savedAccountsResponse
return requireNotNull(savedAccountsResponse) {
"Saved accounts response is null for wallet: $userWalletId"
}
}
var response = savedAccountsResponse ?: createDefaultResponse(userWalletId)

View file

@ -3,9 +3,9 @@ package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.some
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
@ -48,12 +48,16 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor(
.map { response ->
if (response == null) return@map emptySet()
responseCryptoCurrenciesFactory.createCurrencies(
response = response.toUserTokensResponse(),
userWallet = userWallet,
).toSet()
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty(),
userWallet = userWallet,
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
)
}
}
.onEmpty { emit(emptySet()) }
.distinctUntilChanged()
.flowOn(dispatchers.default)
}

View file

@ -0,0 +1,55 @@
package com.tangem.data.account.producer
import arrow.core.Option
import arrow.core.none
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.models.account.Account
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapNotNull
/**
* Default implementation of [SingleAccountProducer] that produces a flow of [Account.CryptoPortfolio]
* for a single account identified by [SingleAccountProducer.Params.accountId].
*
* It uses [SingleAccountListSupplier] to get the list of accounts and filters it to find the
* specific account.
*
* @property params Parameters containing the account ID for which the portfolio is produced.
* @property singleAccountListSupplier Supplier to get the list of accounts.
* @property dispatchers Coroutine dispatcher provider for managing threading.
*/
internal class DefaultSingleAccountProducer @AssistedInject constructor(
@Assisted val params: SingleAccountProducer.Params,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val dispatchers: CoroutineDispatcherProvider,
) : SingleAccountProducer {
override val fallback: Option<Account.CryptoPortfolio>
get() = none()
override fun produce(): Flow<Account.CryptoPortfolio> {
return singleAccountListSupplier(
params = SingleAccountListProducer.Params(userWalletId = params.accountId.userWalletId),
)
.mapNotNull { accountList ->
accountList.accounts.firstOrNull {
it is Account.CryptoPortfolio && params.accountId == it.accountId
} as? Account.CryptoPortfolio
}
.distinctUntilChanged()
.flowOn(dispatchers.default)
}
@AssistedFactory
interface Factory : SingleAccountProducer.Factory {
override fun create(params: SingleAccountProducer.Params): DefaultSingleAccountProducer
}
}

View file

@ -9,13 +9,16 @@ import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.api.safeApiCall
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.common.response.ETAG_HEADER
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.account.models.AccountList
@ -43,7 +46,7 @@ internal class DefaultAccountsCRUDRepository(
private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory,
private val userWalletsStore: UserWalletsStore,
private val userTokensSaver: UserTokensSaver,
private val eTagsStore: ETagsStore,
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>>,
private val convertersContainer: AccountConverterFactoryContainer,
private val dispatchers: CoroutineDispatcherProvider,
) : AccountsCRUDRepository {
@ -90,19 +93,38 @@ internal class DefaultAccountsCRUDRepository(
}
override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) {
val response = withContext(dispatchers.io) {
tangemTechApi.getWalletArchivedAccounts(
walletId = userWalletId.stringValue,
eTag = getETag(userWalletId),
).getOrThrow()
}
val eTag = archivedAccountsETagStore.getSyncOrNull()?.get(key = userWalletId.stringValue)
val store = getArchivedAccountsStore(userWalletId = userWalletId)
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
val archivedAccounts = converter.convertList(input = response.accounts)
val response = safeApiCall(
call = {
val apiResponse = withContext(dispatchers.io) {
tangemTechApi.getWalletArchivedAccounts(
walletId = userWalletId.stringValue,
eTag = eTag,
)
}
store.store(value = archivedAccounts)
saveETag(userWalletId, apiResponse)
apiResponse.bind()
},
onError = {
if (it is HttpException && it.code == HttpException.Code.NOT_MODIFIED) {
null
} else {
throw it
}
},
)
if (response != null) {
val converter = ArchivedAccountConverter(userWalletId = userWalletId)
val archivedAccounts = converter.convertList(input = response.accounts)
store.store(value = archivedAccounts)
}
}
override suspend fun saveAccounts(accountList: AccountList) {
@ -154,9 +176,17 @@ internal class DefaultAccountsCRUDRepository(
return accountListResponse.wallet.totalAccounts.toOption()
}
override fun getTotalAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>> {
override suspend fun getTotalActiveAccountsCountSync(userWalletId: UserWalletId): Option<Int> = option {
val accountListResponse = getAccountsResponseSync(userWalletId = userWalletId)
ensureNotNull(accountListResponse)
return accountListResponse.accounts.size.toOption()
}
override fun getTotalActiveAccountsCount(userWalletId: UserWalletId): Flow<Option<Int>> {
return getAccountsResponseStore(userWalletId = userWalletId).data
.map { it?.wallet?.totalAccounts.toOption() }
.map { it?.accounts?.size.toOption() }
}
override fun getUserWallet(userWalletId: UserWalletId): UserWallet {
@ -167,8 +197,12 @@ internal class DefaultAccountsCRUDRepository(
override fun getUserWalletsSync(): List<UserWallet> = userWalletsStore.userWalletsSync
private suspend fun getETag(userWalletId: UserWalletId): String? {
return eTagsStore.getSyncOrNull(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
private suspend fun saveETag(userWalletId: UserWalletId, apiResponse: ApiResponse<*>) {
val eTag = apiResponse.headers[ETAG_HEADER]?.firstOrNull()
archivedAccountsETagStore.update {
it + (userWalletId.stringValue to eTag)
}
}
private suspend fun getAccountsResponseSync(userWalletId: UserWalletId): GetWalletAccountsResponse? {

View file

@ -9,11 +9,11 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.models.account.DerivationIndex
@ -78,6 +78,8 @@ internal class DefaultMainAccountTokensMigration(
},
)
store.updateData { updatedResponse }
userTokensSaver.push(
userWalletId = userWalletId,
response = updatedResponse.toUserTokensResponse(),

View file

@ -1,14 +1,16 @@
package com.tangem.data.account.utils
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import javax.inject.Inject
@ -19,7 +21,7 @@ import javax.inject.Inject
* @property userWalletsListRepository repository to get user wallet information
* @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts
* @property userTokensResponseFactory factory to create [UserTokensResponse]
* @property cardCryptoCurrencyFactory factory to get default coins for multi-currency wallet
* @property networkFactory factory to create network derivation path
*
[REDACTED_AUTHOR]
*/
@ -27,7 +29,7 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory,
private val userTokensResponseFactory: UserTokensResponseFactory,
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
private val networkFactory: NetworkFactory,
) {
suspend fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse {
@ -59,10 +61,12 @@ internal class DefaultWalletAccountsResponseFactory @Inject constructor(
private fun UserTokensResponse?.orDefault(userWallet: UserWallet?): UserTokensResponse {
if (this != null) return this
return userTokensResponseFactory.createUserTokensResponse(
currencies = userWallet?.let(cardCryptoCurrencyFactory::createDefaultCoinsForMultiCurrencyWallet).orEmpty(),
isGroupedByNetwork = false,
isSortedByBalance = false,
return userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = userWallet?.let {
AccountId.forCryptoPortfolio(userWalletId = it.walletId, derivationIndex = DerivationIndex.Main)
},
)
}
}

View file

@ -6,20 +6,6 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.models.wallet.UserWalletId
/** Flattens the tokens from all wallet accounts into a single list */
internal fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
return accounts.flatMap { it.tokens.orEmpty() }
}
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
internal fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
group = wallet.group,
sort = wallet.sort,
tokens = flattenTokens(),
)
}
/**
* Assigns tokens from a [UserTokensResponse] to the wallet accounts in the [GetWalletAccountsResponse]
*

View file

@ -29,6 +29,8 @@ class DefaultSingleAccountListFetcherTest {
// Arrange
val params = SingleAccountListFetcher.Params(userWalletId = userWalletId)
coEvery { walletAccountsFetcher.fetch(userWalletId) } returns mockk()
// Act
val actual = fetcher.invoke(params)

View file

@ -199,7 +199,7 @@ class DefaultWalletAccountsFetcherTest {
@Test
fun `fetch should call error handler when getWalletAccounts returns error`() = runTest {
// Arrange
val savedAccountsResponse = null
val savedAccountsResponse = createGetWalletAccountsResponse(userWalletId)
val apiError = ApiResponse.Error(ApiResponseError.NetworkException())
accountsResponseStoreFlow.value = savedAccountsResponse
@ -212,7 +212,7 @@ class DefaultWalletAccountsFetcherTest {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = savedAccountsResponse,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)
@ -231,7 +231,7 @@ class DefaultWalletAccountsFetcherTest {
fetchWalletAccountsErrorHandler.handle(
error = apiError.cause,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = savedAccountsResponse,
pushWalletAccounts = any(),
storeWalletAccounts = any(),
)

View file

@ -3,13 +3,13 @@ package com.tangem.data.account.fetcher
import com.tangem.data.account.converter.createGetWalletAccountsResponse
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.clearMocks
@ -53,6 +53,8 @@ class FetchWalletAccountsErrorHandlerTest {
@Test
fun `does not update accounts when response is up to date`() = runTest {
// Arrange
val response = createGetWalletAccountsResponse(userWalletId)
val error = ApiResponseError.HttpException(
code = Code.NOT_MODIFIED,
message = "Not Modified",
@ -63,7 +65,7 @@ class FetchWalletAccountsErrorHandlerTest {
handler.handle(
error = error,
userWalletId = userWalletId,
savedAccountsResponse = null,
savedAccountsResponse = response,
pushWalletAccounts = pushWalletAccounts,
storeWalletAccounts = storeWalletAccounts,
)

View file

@ -10,7 +10,6 @@ import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStore
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -54,7 +53,7 @@ class DefaultAccountsCRUDRepositoryTest {
private val userWalletsStore: UserWalletsStore = mockk()
private val userTokensSaver: UserTokensSaver = mockk()
private val eTagsStore: ETagsStore = mockk()
private val archivedAccountsETagStore: RuntimeStateStore<Map<String, String?>> = mockk(relaxUnitFun = true)
private val convertersContainer: AccountConverterFactoryContainer = mockk()
private val accountListConverter: AccountListConverter = mockk()
@ -67,7 +66,7 @@ class DefaultAccountsCRUDRepositoryTest {
archivedAccountsStoreFactory = archivedAccountsStoreFactory,
userWalletsStore = userWalletsStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
archivedAccountsETagStore = archivedAccountsETagStore,
convertersContainer = convertersContainer,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -532,7 +531,7 @@ class DefaultAccountsCRUDRepositoryTest {
val archivedAccount = ArchivedAccountConverter(userWalletId).convert(accountDTO)
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
coEvery {
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
@ -546,9 +545,10 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(actual).containsExactly(archivedAccount)
coVerifyOrder {
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
archivedAccountsETagStore.getSyncOrNull()
archivedAccountsStoreFactory.create(userWalletId)
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
archivedAccountsETagStore.update(any())
}
}
@ -558,7 +558,7 @@ class DefaultAccountsCRUDRepositoryTest {
val eTag = "etag123"
val exception = Exception("API error")
coEvery { eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts) } returns eTag
coEvery { archivedAccountsETagStore.getSyncOrNull() } returns mapOf(userWalletId.stringValue to eTag)
coEvery { tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag) } throws exception
// Act
@ -570,7 +570,7 @@ class DefaultAccountsCRUDRepositoryTest {
Truth.assertThat(archivedAccountsStore.getSyncOrNull()).isNull()
coVerifyOrder {
eTagsStore.getSyncOrNull(userWalletId, ETagsStore.Key.WalletAccounts)
archivedAccountsETagStore.getSyncOrNull()
tangemTechApi.getWalletArchivedAccounts(userWalletId.stringValue, eTag)
}
}
@ -644,15 +644,15 @@ class DefaultAccountsCRUDRepositoryTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTotalAccountsCountSync {
inner class GetTotalActiveAccountsCountSync {
@Test
fun `getTotalAccountsCountSync returns None if account list response is null`() = runTest {
fun `getTotalActiveAccountsCountSync returns None if account list response is null`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val actual = repository.getTotalAccountsCountSync(userWalletId)
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
// Assert
Truth.assertThat(actual).isEqualTo(None)
@ -664,20 +664,22 @@ class DefaultAccountsCRUDRepositoryTest {
}
@Test
fun `getTotalAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
fun `getTotalActiveAccountsCountSync returns Some with totalAccounts when response is valid`() = runTest {
// Arrange
val totalAccounts = 5
val response = mockk<GetWalletAccountsResponse> {
every { this@mockk.wallet.totalAccounts } returns totalAccounts
val response = createGetWalletAccountsResponse(userWalletId).let {
it.copy(
wallet = it.wallet.copy(totalAccounts = totalAccounts),
)
}
accountsResponseStoreFlow.value = response
// Act
val actual = repository.getTotalAccountsCountSync(userWalletId)
val actual = repository.getTotalActiveAccountsCountSync(userWalletId)
// Assert
val expected = totalAccounts.toOption()
val expected = 1.toOption()
Truth.assertThat(actual).isEqualTo(expected)
verifyOrder {
@ -689,15 +691,15 @@ class DefaultAccountsCRUDRepositoryTest {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class GetTotalAccountsCount {
inner class GetTotalActiveAccountsCount {
@Test
fun `getTotalAccountsCount emits 0 when account list response is null`() = runTest {
fun `getTotalActiveAccountsCount emits 0 when account list response is null`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val flow = repository.getTotalAccountsCount(userWalletId)
val flow = repository.getTotalActiveAccountsCount(userWalletId)
val actual = getEmittedValues(flow)
// Assert
@ -710,21 +712,23 @@ class DefaultAccountsCRUDRepositoryTest {
}
@Test
fun `getTotalAccountsCount emits correct value when response is valid`() = runTest {
fun `getTotalActiveAccountsCount emits correct value when response is valid`() = runTest {
// Arrange
val totalAccounts = 7
val response = mockk<GetWalletAccountsResponse> {
every { this@mockk.wallet.totalAccounts } returns totalAccounts
val response = createGetWalletAccountsResponse(userWalletId).let {
it.copy(
wallet = it.wallet.copy(totalAccounts = totalAccounts),
)
}
accountsResponseStoreFlow.value = response
// Act
val flow = repository.getTotalAccountsCount(userWalletId)
val flow = repository.getTotalActiveAccountsCount(userWalletId)
val actual = getEmittedValues(flow)
// Assert
Truth.assertThat(actual).containsExactly(totalAccounts.toOption())
Truth.assertThat(actual).containsExactly(1.toOption())
verifyOrder {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data

View file

@ -7,10 +7,10 @@ import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.account.utils.toUserTokensResponse
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
@ -183,6 +183,8 @@ class DefaultMainAccountTokensMigrationTest {
accountsResponseStoreFlow.value = response
coEvery { accountsResponseStore.updateData(any()) } returns mockk()
// Act
val actual = migration.migrate(userWalletId, derivationIndex)
@ -199,6 +201,7 @@ class DefaultMainAccountTokensMigrationTest {
coVerifySequence {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
accountsResponseStore.updateData(any())
userTokensSaver.push(
userWalletId = userWalletId,
response = migratedResponse.toUserTokensResponse(),

View file

@ -3,8 +3,8 @@ package com.tangem.data.account.utils
import com.google.common.truth.Truth
import com.tangem.data.account.converter.CryptoPortfolioConverter
import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
import com.tangem.data.common.currency.UserTokensResponseFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.account.models.AccountList
@ -27,13 +27,13 @@ class DefaultWalletAccountsResponseFactoryTest {
private val cryptoPortfolioCF = mockk<CryptoPortfolioConverter.Factory>()
private val cryptoPortfolioConverter = mockk<CryptoPortfolioConverter>()
private val userTokensResponseFactory = mockk<UserTokensResponseFactory>()
private val cardCryptoCurrencyFactory = mockk<CardCryptoCurrencyFactory>()
private val networkFactory = mockk<NetworkFactory>()
private val factory = DefaultWalletAccountsResponseFactory(
userWalletsListRepository = userWalletsListRepository,
cryptoPortfolioCF = cryptoPortfolioCF,
userTokensResponseFactory = userTokensResponseFactory,
cardCryptoCurrencyFactory = cardCryptoCurrencyFactory,
networkFactory = networkFactory,
)
private val userWalletId = UserWalletId("011")
@ -50,7 +50,7 @@ class DefaultWalletAccountsResponseFactoryTest {
cryptoPortfolioCF,
cryptoPortfolioConverter,
userTokensResponseFactory,
cardCryptoCurrencyFactory,
networkFactory,
)
}
@ -65,10 +65,10 @@ class DefaultWalletAccountsResponseFactoryTest {
coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList()
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = null,
networkFactory = networkFactory,
accountId = null,
)
} returns userTokensResponse
@ -89,10 +89,10 @@ class DefaultWalletAccountsResponseFactoryTest {
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = null,
networkFactory = networkFactory,
accountId = null,
)
}
}
@ -104,27 +104,24 @@ class DefaultWalletAccountsResponseFactoryTest {
every { walletId } returns userWalletId
}
val defaultCoins = listOf(mockk<CryptoCurrency.Coin>())
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = listOf(mockk(relaxed = true)),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
val accountsDTO = createWalletAccountDTO(userWalletId)
every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO)
@ -147,11 +144,10 @@ class DefaultWalletAccountsResponseFactoryTest {
coVerifyOrder {
userWalletsListRepository.userWalletsSync()
cryptoPortfolioConverter.convertListBack(accounts)
cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet)
userTokensResponseFactory.createUserTokensResponse(
currencies = defaultCoins,
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
}
}
@ -162,22 +158,26 @@ class DefaultWalletAccountsResponseFactoryTest {
val userWallet = mockk<UserWallet>(relaxed = true) {
every { walletId } returns userWalletId
}
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet)
every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList()
val defaultResponse = UserTokensResponse(
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
every {
userTokensResponseFactory.createUserTokensResponse(
currencies = emptyList(),
isGroupedByNetwork = false,
isSortedByBalance = false,
userTokensResponseFactory.createDefaultResponse(
userWallet = userWallet,
networkFactory = networkFactory,
accountId = accounts.first().accountId,
)
} returns defaultResponse
val accounts = AccountList.empty(userWallet.walletId).accounts
.filterIsInstance<Account.CryptoPortfolio>()
every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList()
// Act

View file

@ -4,6 +4,8 @@ import com.google.common.truth.Truth
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.datasource.api.tangemTech.models.account.flattenTokens
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId

View file

@ -1,5 +1,6 @@
package com.tangem.data.common.account
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.models.wallet.UserWalletId
/**
@ -11,5 +12,8 @@ interface WalletAccountsFetcher {
/** Fetch wallet accounts by [userWalletId] */
@Throws
suspend fun fetch(userWalletId: UserWalletId)
suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse
/** Get saved wallet accounts by [userWalletId] */
suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse?
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.data.common.network.NetworkFactory
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
@ -18,7 +19,7 @@ class CryptoCurrencyFactory(
private val excludedBlockchains: ExcludedBlockchains,
) {
private val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) }
val networkFactory by lazy(LazyThreadSafetyMode.NONE) { NetworkFactory(excludedBlockchains) }
@Suppress("LongParameterList") // Yep, it's long
fun createToken(
@ -48,6 +49,7 @@ class CryptoCurrencyFactory(
blockchain: Blockchain,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): CryptoCurrency.Token? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
@ -58,6 +60,7 @@ class CryptoCurrencyFactory(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
val id = getTokenId(network, sdkToken)
@ -74,11 +77,16 @@ class CryptoCurrencyFactory(
)
}
fun createCoin(chainId: Int, extraDerivationPath: String?, userWallet: UserWallet): CryptoCurrency.Coin? {
fun createCoin(
chainId: Int,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): CryptoCurrency.Coin? {
val blockchain: Blockchain? = Chain.entries.find { it.id == chainId }?.blockchain
return if (blockchain != null) {
createCoin(blockchain, extraDerivationPath, userWallet)
createCoin(blockchain, extraDerivationPath, userWallet, accountIndex)
} else {
Timber.e("Unable to get blockchain from chainId == $chainId")
null
@ -89,6 +97,7 @@ class CryptoCurrencyFactory(
blockchain: Blockchain,
extraDerivationPath: String?,
userWallet: UserWallet,
accountIndex: DerivationIndex? = null,
): CryptoCurrency.Coin? {
if (blockchain == Blockchain.Unknown) {
Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain")
@ -99,6 +108,7 @@ class CryptoCurrencyFactory(
blockchain = blockchain,
extraDerivationPath = extraDerivationPath,
userWallet = userWallet,
accountIndex = accountIndex,
) ?: return null
return createCoin(network)

View file

@ -1,14 +1,16 @@
package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.tokens.getDefaultWalletBlockchains
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
@ -23,10 +25,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency
* @property userWalletsStore user wallets store
* @property userTokensResponseStore user tokens response store
*/
@Suppress("LongParameterList")
internal class DefaultCardCryptoCurrencyFactory(
private val demoConfig: DemoConfig,
private val excludedBlockchains: ExcludedBlockchains,
private val userWalletsStore: UserWalletsStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val userTokensResponseStore: UserTokensResponseStore,
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
) : CardCryptoCurrencyFactory {
@ -99,25 +104,7 @@ internal class DefaultCardCryptoCurrencyFactory(
override fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List<CryptoCurrency.Coin> {
require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" }
val blockchains = when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) {
demoConfig.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
if (card.isTestCard) {
blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() }
}
blockchainsInternal
}
is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
val blockchains = getDefaultWalletBlockchains(userWallet, demoConfig)
return blockchains.mapNotNull {
cryptoCurrencyFactory.createCoin(
@ -152,15 +139,32 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
networks: Set<Network>,
): Map<Network, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
val existingNetworkWithCurrencies = responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
userWallet = userWallet,
)
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty().filter { token ->
networks.any {
it.backendId == token.networkId && it.derivationPath.value == token.derivationPath
}
},
userWallet = userWallet,
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
)
}
} else {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token ->
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
},
userWallet = userWallet,
)
}
.groupBy(CryptoCurrency::network)
return networks.associateWith { emptyList<CryptoCurrency>() } + existingNetworkWithCurrencies
@ -170,15 +174,28 @@ internal class DefaultCardCryptoCurrencyFactory(
userWallet: UserWallet,
rawIds: Set<Network.RawID>,
): Map<Network.RawID, List<CryptoCurrency>> {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
val networkIds = rawIds.map { it.toBlockchain().toNetworkId() }
return responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token -> token.networkId in networkIds },
userWallet = userWallet,
)
return if (accountsFeatureToggles.isFeatureEnabled) {
val response = walletAccountsFetcher.getSaved(userWallet.walletId)
?: return emptyMap()
response.accounts.flatMapTo(hashSetOf()) { accountDTO ->
responseCryptoCurrenciesFactory.createCurrencies(
tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds },
userWallet = userWallet,
accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull(),
)
}
} else {
val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId)
?: return emptyMap()
responseCryptoCurrenciesFactory.createCurrencies(
tokens = response.tokens.filter { token -> token.networkId in networkIds },
userWallet = userWallet,
)
}
.groupBy { it.network.id.rawId }
}

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