Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-22 10:09:11 +02:00
commit e4754fed3b
483 changed files with 9315 additions and 4084 deletions

View file

@ -99,6 +99,7 @@ dependencies {
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.analytics)
implementation(projects.domain.visa)
implementation(projects.domain.onboarding)
@ -227,8 +228,8 @@ dependencies {
implementation(projects.features.usedesk.impl)
implementation(projects.features.hotWallet.api)
implementation(projects.features.hotWallet.impl)
implementation(projects.features.kyc.api)
//TODO disable for release because of the permissions
// implementation(projects.features.kyc.api)
// implementation(projects.features.kyc.impl)
implementation(projects.features.welcome.api)
implementation(projects.features.welcome.impl)
@ -238,6 +239,10 @@ dependencies {
implementation(projects.features.home.impl)
implementation(projects.features.account.api)
implementation(projects.features.account.impl)
implementation(projects.features.tangempay.details.api)
implementation(projects.features.tangempay.details.impl)
implementation(projects.features.tangempay.main.api)
implementation(projects.features.tangempay.main.impl)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.tokenRecieve.impl)
@ -351,7 +356,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
mockedImplementation(deps.chucker)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule {
ApiConfig.ID.TangemTech,
ApiConfig.ID.Express,
ApiConfig.ID.TangemPay,
ApiConfig.ID.StakeKit,
)
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.*
import com.tangem.features.tokendetails.impl.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 com.tangem.features.staking.impl.R as StakingImplR
import androidx.compose.ui.test.hasTestTag as withTestTag
class StakingDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingDetailsPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
}
val stakingTitle: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val bannerImage: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.BANNER_IMAGE)
useUnmergedTree = true
}
val bannerText: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.BANNER_TEXT)
useUnmergedTree = true
}
val annualPercentageRate: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_annual_percentage_rate))
useUnmergedTree = true
}
val availableBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_available))
useUnmergedTree = true
}
val unbondingPeriodBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_unbonding_period))
useUnmergedTree = true
}
val rewardClaimingBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_reward_claiming))
useUnmergedTree = true
}
val rewardScheduleBlock: KNode = child {
hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK))
hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME)
hasText(getResourceString(StakingImplR.string.staking_details_reward_schedule))
useUnmergedTree = true
}
val rewardsBlock: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK)
useUnmergedTree = true
}
val rewardsBlockTitle: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK_TITLE)
useUnmergedTree = true
}
val rewardsBlockText: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK_TEXT)
useUnmergedTree = true
}
val yourStakesTitle: KNode = child {
hasText(getResourceString(StakingImplR.string.staking_your_stakes))
useUnmergedTree = true
}
val activeStakingBlock: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK)
useUnmergedTree = true
}
val toSText: KNode = child {
hasTestTag(StakingDetailsScreenTestTags.TOS_TEXT)
useUnmergedTree = true
}
val stakeMoreButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.staking_stake_more))
useUnmergedTree = true
}
val stakeButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_stake))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onStakingDetailsScreen(function: StakingDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,51 @@
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.StakingSendDetailsScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendDetailsPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val primaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
val validatorBlock: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK)
useUnmergedTree = true
}
val networkFeeBlock: KNode = child {
hasTestTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK)
useUnmergedTree = true
}
val stakeButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_stake))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,78 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.StakingSendScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.send.v2.impl.R as SendR
import androidx.compose.ui.test.hasTestTag as withTestTag
class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StakingSendPageObject>(semanticsProvider = semanticsProvider) {
val screenContainer: KNode = child {
hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER)
}
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
useUnmergedTree = true
}
val amountContainerTitle: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE)
useUnmergedTree = true
}
val amountContainerText: KNode = child {
hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT)
useUnmergedTree = true
}
val amountInputTextField: KNode = child {
hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD)
useUnmergedTree = true
}
val secondaryAmount: KNode = child {
hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT)
useUnmergedTree = true
}
val currencyButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON))
useUnmergedTree = true
}
val fiatButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON)
hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON))
useUnmergedTree = true
}
val maxButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.MAX_BUTTON)
useUnmergedTree = true
}
val previousButton: KNode = child {
hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON)
useUnmergedTree = true
}
val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_next))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -3,10 +3,7 @@ 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.SwapTokenScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
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
@ -32,7 +29,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
val networkFeeBlock: KNode = child {
hasTestTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK)
hasTestTag(BaseBlockTestTags.BLOCK)
useUnmergedTree = true
}

View file

@ -4,15 +4,16 @@ import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.common.utils.LazyListItemNode
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
import com.tangem.features.tokendetails.impl.R
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
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.compose.node.element.lazylist.KLazyListNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
@ -21,6 +22,61 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER)
}
val availableStakingBlock: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK)
useUnmergedTree = true
}
val stakingBlock: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_BLOCK)
useUnmergedTree = true
}
val availableStakingBlockTitle: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE)
useUnmergedTree = true
}
val availableStakingBlockText: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT)
useUnmergedTree = true
}
val availableStakingBlockCurrencyIcon: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON)
useUnmergedTree = true
}
val stakeButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_stake))
useUnmergedTree = true
}
val stakingFiatAmount: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT)
useUnmergedTree = true
}
val stakingDot: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT)
useUnmergedTree = true
}
val stakingTokenAmount: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT)
useUnmergedTree = true
}
val stakingChevronIcon: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON)
useUnmergedTree = true
}
val stakingTitle: KNode = child {
hasText(getResourceString(R.string.staking_native))
}
val title: KNode = child {
hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE)
}

View file

@ -371,13 +371,6 @@ class BuyTokenTest : BaseTestCase() {
step("Open 'Select Provider' bottom sheet") {
onBuyTokenDetailsScreen { providerTitle.performClick() }
}
step("Assert unavailable provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(WAIT_UNTIL_TIMEOUT) {
unavailableProviderItem.assertIsDisplayed()
}
}
}
step("Assert available provider name is displayed") {
onSelectProviderBottomSheet {
flakySafely(WAIT_UNTIL_TIMEOUT) {

View file

@ -31,6 +31,7 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -55,6 +56,7 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -105,6 +107,7 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -140,6 +143,7 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }
@ -192,6 +196,7 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Swipe to 'Organize tokens' button") {
swipeUp()
swipeUp()
}
step("Click 'Organize tokens' button") {
onMainScreen { organizeTokensButton().clickWithAssertion() }

View file

@ -0,0 +1,379 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class StakingTest : BaseTestCase() {
@AllureId("3558")
@DisplayName("Staking: validate staking block on 'Token details' screen")
@Test
fun validateStakingBlockTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Staking block' is displayed") {
onTokenDetailsScreen { stakingBlock.assertIsDisplayed() }
}
step("Assert 'Staking title' is displayed") {
onTokenDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert 'Staking fiat amount' is displayed") {
onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() }
}
step("Assert 'Staking dot' is displayed") {
onTokenDetailsScreen { stakingDot.assertIsDisplayed() }
}
step("Assert 'Staking token amount' is displayed") {
onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() }
}
step("Assert 'Staking block chevron icon' is displayed") {
onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() }
}
}
}
@AllureId("3550")
@DisplayName("Staking: validate staking more screens")
@Test
fun validateStakingMoreScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Staked"
val stakingAmount = "1"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Click on 'Staking block'") {
onTokenDetailsScreen { stakingBlock.clickWithAssertion() }
}
step("Assert 'Title' is displayed") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' is displayed") {
onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() }
}
step("Assert 'Rewards block' title is displayed") {
onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() }
}
step("Assert 'Rewards block' text is displayed") {
onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() }
}
step("Assert 'Active staking block' is displayed") {
onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() }
}
step("Assert 'Your stakes' title is displayed") {
onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
step("Assert 'Stake more' button is displayed") {
onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() }
}
step("Click 'Stake more' button") {
onStakingDetailsScreen { stakeMoreButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
}
}
}
@AllureId("3548")
@DisplayName("Staking: validate staking screens")
@Test
fun validateStakingScreensTest() {
val tokenTitle = "POL (ex-MATIC)"
val balance = TOTAL_BALANCE
val scenarioName = "staking_eth_pol_balances_android"
val scenarioState = "Started"
val stakingAmount = "1"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = $balance") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert 'Token details screen' open") {
onTokenDetailsScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Available staking block' is displayed") {
onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() }
}
step("Assert 'Available staking block' title is displayed") {
onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() }
}
step("Assert 'Available staking block' text is displayed") {
onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() }
}
step("Assert 'Available staking block' currency icon is displayed") {
onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() }
}
step("Click on 'Stake' button") {
onTokenDetailsScreen { stakeButton.clickWithAssertion() }
}
step("Assert 'Title' is displayed") {
onStakingDetailsScreen { stakingTitle.assertIsDisplayed() }
}
step("Assert banner image is displayed") {
onStakingDetailsScreen { bannerImage.assertIsDisplayed() }
}
step("Assert banner text is displayed") {
onStakingDetailsScreen { bannerText.assertIsDisplayed() }
}
step("Assert 'Annual percentage rate' is displayed") {
onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() }
}
step("Assert 'Available' block is displayed") {
onStakingDetailsScreen { availableBlock.assertIsDisplayed() }
}
step("Assert 'Unbonding Period' block is displayed") {
onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() }
}
step("Assert 'Reward claiming' block is displayed") {
onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() }
}
step("Assert 'Reward schedule' block is displayed") {
onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() }
}
step("Assert 'ToS' text is displayed") {
onStakingDetailsScreen { toSText.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingDetailsScreen { stakeButton.assertIsDisplayed() }
}
step("Click 'Stake' button") {
onStakingDetailsScreen { stakeButton.performClick() }
}
step("Assert 'Send' screen is displayed") {
onStakingSendScreen { screenContainer.assertIsDisplayed() }
}
step("Assert 'Send' screen title is displayed") {
onStakingSendScreen { title.assertIsDisplayed() }
}
step("Assert amount container title is displayed") {
onStakingSendScreen { amountContainerTitle.assertIsDisplayed() }
}
step("Assert amount container text is displayed") {
onStakingSendScreen { amountContainerText.assertIsDisplayed() }
}
step("Assert input text field is displayed") {
onStakingSendScreen { amountInputTextField.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendScreen { secondaryAmount.assertIsDisplayed() }
}
step("Type '$stakingAmount' in input text field") {
onStakingSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(stakingAmount)
}
}
step("Assert input text field has value: '$stakingAmount'") {
onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert currency button is displayed") {
onStakingSendScreen { currencyButton.assertIsDisplayed() }
}
step("Assert fiat button is displayed") {
onStakingSendScreen { fiatButton.assertIsDisplayed() }
}
step("Assert 'Max' button is displayed") {
onStakingSendScreen { maxButton.assertIsDisplayed() }
}
step("Assert previous button is displayed") {
onStakingSendScreen { previousButton.assertIsDisplayed() }
}
step("Assert 'Next' button is displayed") {
onStakingSendScreen { nextButton.assertIsDisplayed() }
}
step("Click on 'Next' button") {
onStakingSendScreen { nextButton.performClick() }
}
step("Assert 'Send details' screen title is displayed") {
onStakingSendDetailsScreen { title.assertIsDisplayed() }
}
step("Assert primary amount is displayed") {
onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() }
}
step("Assert secondary amount is displayed") {
onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() }
}
step("Assert 'Validator' block is displayed") {
onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() }
}
step("Assert 'Network Fee' block is displayed") {
onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() }
}
step("Assert 'Stake' button is displayed") {
onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() }
}
}
}
}

@ -1 +1 @@
Subproject commit 23aae9e3496d89a021ac9a0833b54b49635bb193
Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c

View file

@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
@ -142,4 +145,10 @@ interface ApplicationEntryPoint {
fun getApiConfigsManager(): ApiConfigsManager
fun getUserTokensResponseStore(): UserTokensResponseStore
fun getUserWalletsListRepository(): UserWalletsListRepository
fun getTangemHotSdk(): TangemHotSdk
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
}

View file

@ -7,6 +7,8 @@ import androidx.work.WorkerParameters
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
@ -17,13 +19,22 @@ class LockTimerWorker @AssistedInject constructor(
@Assisted params: WorkerParameters,
private val settingsRepository: SettingsRepository,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
Timber.i("onStart job")
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
userWalletsListManagerLockable.lock()
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.lockAllWallets()
.onRight {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
} else {
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
userWalletsListManagerLockable.lock()
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
Timber.i("onStart job complete")
return Result.success()
}

View file

@ -10,6 +10,8 @@ import com.tangem.common.routing.AppRoute
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
import kotlinx.coroutines.CoroutineScope
@ -25,6 +27,8 @@ internal class LockUserWalletsTimer(
private val settingsRepository: SettingsRepository,
private val duration: Duration = with(Duration) { 5.minutes },
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val coroutineScope: CoroutineScope,
) : LifecycleOwner by context as LifecycleOwner,
DefaultLifecycleObserver {
@ -108,20 +112,33 @@ internal class LockUserWalletsTimer(
delay(duration)
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val userWallets = userWalletsListRepository.userWalletsSync()
if (userWallets.isNotEmpty()) {
userWalletsListRepository.lockAllWallets()
.onLeft {
start()
}
.onRight {
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
} else {
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
Timber.i(
"""
Timber.i(
"""
Finished
|- Millis passed: ${currentTime - startTime}
""".trimIndent(),
)
""".trimIndent(),
)
userWalletsListManager.lock()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
userWalletsListManager.lock()
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
}
}

View file

@ -40,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
@ -48,7 +49,10 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.google.GoogleServicesHelper
@ -188,6 +192,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler
@Inject
internal lateinit var userWalletsListRepository: UserWalletsListRepository
@Inject
internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -271,6 +284,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
settingsRepository = settingsRepository,
userWalletsListManager = userWalletsListManager,
coroutineScope = mainScope,
userWalletsListRepository = userWalletsListRepository,
hotWalletFeatureToggles = hotWalletFeatureToggles,
)
initIntentHandlers()
@ -429,6 +444,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
// TODO refactor this method to return a route instead of navigating directly
if (hotWalletFeatureToggles.isHotWalletEnabled) {
navigateToInitialScreenIfNeededNew(intentWhichStartedActivity)
return
}
val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA])
val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial }
@ -448,9 +469,66 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
}
}
@Deprecated("Refactor this method to return a route instead of navigating directly")
private fun navigateToInitialScreenIfNeededNew(intentWhichStartedActivity: Intent?) {
lifecycleScope.launch {
val userWallets = userWalletsListRepository.userWalletsSync()
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWallets.isEmpty()) {
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
val route = if (shouldShowTos) {
AppRoute.Disclaimer(isTosAccepted = false)
} else {
AppRoute.Home(launchMode = launchMode)
}
store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = false,
)
} else {
if (userWallets.any { it.isLocked }) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(
launchMode = launchMode,
intent = intentWhichStartedActivity?.let(::SerializableIntent),
),
)
}
} else {
store.dispatchNavigationAction {
replaceAll(AppRoute.Wallet)
}
}
intentProcessor.handleIntent(
intent = intentWhichStartedActivity,
isFromForeground = false,
skipNavigationHandlers = true,
)
}
if (intent != null) {
handleDeepLink(intent = intent, isFromOnNewIntent = false)
}
viewModel.checkForUnfinishedBackup()
}
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
// Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs
if (tangemPayFeatureToggles.isTangemPayEnabled) {
store.dispatchNavigationAction {
replaceAll(AppRoute.TangemPayDetails)
}
} else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Welcome(

View file

@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val userTokensResponseStore: UserTokensResponseStore
get() = entryPoint.getUserTokensResponseStore()
private val userWalletsListRepository
get() = entryPoint.getUserWalletsListRepository()
private val tangemHotSdk
get() = entryPoint.getTangemHotSdk()
private val hotWalletFeatureToggles
get() = entryPoint.getHotWalletFeatureToggles()
// endregion
private val appScope = MainScope()
@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
uiMessageSender = uiMessageSender,
coldUserWalletBuilderFactory = coldUserWalletBuilderFactory,
userTokensResponseStore = userTokensResponseStore,
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
hotWalletFeatureToggles = hotWalletFeatureToggles,
),
),
)

View file

@ -2,12 +2,12 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.card.analytics.IntroductionProcess
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.features.home.impl.analytics.IntroductionProcess
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.demo.DemoHelper

View file

@ -26,10 +26,9 @@ internal object LegacyMiddleware {
{ action ->
when (action) {
is LegacyAction.PrepareDetailsScreen -> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
userWalletsListManager.selectedUserWallet
selectedUserWallet()
.distinctUntilChanged()
.onEach { selectedUserWallet ->
val initializedAppSettingsStateContent = initializeAppSettingsState(
@ -52,6 +51,16 @@ internal object LegacyMiddleware {
}
}
private fun selectedUserWallet(): Flow<UserWallet> {
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
} else {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.selectedUserWallet
}
}
/**
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
* previously it was initialized in runBlocking and blocked details screen
@ -64,6 +73,8 @@ internal object LegacyMiddleware {
selectedAppCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
?: AppThemeMode.DEFAULT,
requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(),
useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(),
isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository)
.getBalanceHidingSettings().isHidingEnabledInSettings,
needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true,

View file

@ -6,7 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented
// [REDACTED_JIRA]
@ -28,10 +27,6 @@ internal class RuntimeUserWalletsStore(
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}
override suspend fun getAllSyncOrNull(): List<UserWallet>? {
return userWalletsListManager.userWallets.firstOrNull()
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,

View file

@ -0,0 +1,50 @@
package com.tangem.tap.data
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.core.wallets.UserWalletsListRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class UserWalletsStoreRepositoryProxy(
private val userWalletsListRepository: UserWalletsListRepository,
) : UserWalletsStore {
override val selectedUserWalletOrNull: UserWallet?
get() = userWalletsListRepository.selectedUserWallet.value
override val userWallets: Flow<List<UserWallet>>
get() = flow {
userWalletsListRepository.load()
userWalletsListRepository.userWallets.collect {
emit(requireNotNull(it))
}
}
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
return userWalletsListRepository.userWallets.value?.find { it.walletId == key }
}
override fun getSyncStrict(key: UserWalletId): UserWallet {
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return catching {
val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId }
requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" }
val updatedUserWallet = update(userWallet)
userWalletsListRepository.saveWithoutLock(
userWallet = updatedUserWallet,
canOverride = true,
)
updatedUserWallet
}
}
}

View file

@ -2,7 +2,10 @@ package com.tangem.tap.di.data
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.data.RuntimeUserWalletsStore
import com.tangem.tap.data.UserWalletsStoreRepositoryProxy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -15,7 +18,15 @@ internal object UserWalletsStoreModule {
@Provides
@Singleton
fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore {
return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
fun provideUserWalletsStore(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): UserWalletsStore {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
UserWalletsStoreRepositoryProxy(userWalletsListRepository)
} else {
RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
}
}
}

View file

@ -1,10 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -44,4 +41,12 @@ internal object AccountDomainModule {
): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository)
}
@Provides
@Singleton
fun provideGetUnoccupiedAccountIndexUseCase(
accountsCRUDRepository: AccountsCRUDRepository,
): GetUnoccupiedAccountIndexUseCase {
return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository)
}
}

View file

@ -7,11 +7,13 @@ import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.card.DefaultResetCardUseCase
@ -42,9 +44,16 @@ internal object CardDomainModule {
}
@Provides
@Singleton
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
fun provideIsNeedToBackupUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides

View file

@ -3,7 +3,9 @@ package com.tangem.tap.di.domain
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import com.tangem.tap.domain.scanCard.LegacyScanProcessor
@ -31,7 +33,15 @@ internal object CardLegacyDomainModule {
@Provides
@Singleton
fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase {
return GenerateWalletNameUseCase(userWalletsListManager)
fun providesWalletNameGenerateUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GenerateWalletNameUseCase {
return GenerateWalletNameUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
}

View file

@ -13,6 +13,8 @@ import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -81,10 +83,14 @@ object MarketsDomainModule {
@Singleton
fun provideFilterNetworksUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
excludedBlockchains: ExcludedBlockchains,
): FilterAvailableNetworksForWalletUseCase {
return FilterAvailableNetworksForWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -18,7 +18,6 @@ import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -98,11 +97,11 @@ internal object TokensDomainModule {
@Singleton
fun provideGetTokenListUseCase(
currenciesRepository: CurrenciesRepository,
baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations,
currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetTokenListUseCase {
return GetTokenListUseCase(
currenciesRepository = currenciesRepository,
currenciesStatusesOperations = baseCurrenciesStatusesOperations,
currenciesStatusesOperations = currenciesStatusesOperations,
)
}
@ -370,9 +369,9 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideGetWalletTotalBalanceUseCase(
baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations,
currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations)
return GetWalletTotalBalanceUseCase(currenciesStatusesOperations)
}
@Provides
@ -400,42 +399,6 @@ internal object TokensDomainModule {
return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers)
}
@Provides
@Singleton
fun provideBaseCurrenciesStatusesOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideBaseCurrencyStatusOperations(

View file

@ -191,6 +191,15 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun providePrepareAndSignUseCase(
transactionRepository: TransactionRepository,
cardSdkConfigRepository: CardSdkConfigRepository,
): PrepareAndSignUseCase {
return PrepareAndSignUseCase(transactionRepository, cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideSignUseCase(

View file

@ -10,11 +10,13 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -31,18 +33,30 @@ internal object WalletsDomainModule {
@Provides
fun providesUserWalletsSyncDelegate(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): UserWalletsSyncDelegate {
return DefaultUserWalletsSyncDelegate(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
return GetWalletsUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetWalletsUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetWalletsUseCase {
return GetWalletsUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@ -50,37 +64,79 @@ internal object WalletsDomainModule {
fun providesWalletNameMigrationUseCase(
userWalletsListManager: UserWalletsListManager,
walletNamesMigrationRepository: WalletNamesMigrationRepository,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): WalletNameMigrationUseCase {
return WalletNameMigrationUseCase(
userWalletsListManager = userWalletsListManager,
walletNamesMigrationRepository = walletNamesMigrationRepository,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase {
return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetUserWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetUserWalletUseCase {
return GetUserWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesGetSelectedWalletSyncUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager)
return GetSelectedWalletSyncUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetSelectedWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase {
return SaveWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesSaveWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
walletsRepository: WalletsRepository,
): SaveWalletUseCase {
return SaveWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase {
return GenerateBuyTangemCardLinkUseCase()
}
@Provides
@ -99,15 +155,30 @@ internal object WalletsDomainModule {
@Singleton
fun providesSelectWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
reduxStateHolder: ReduxStateHolder,
): SelectWalletUseCase {
return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder)
return SelectWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
reduxStateHolder = reduxStateHolder,
)
}
@Provides
@Singleton
fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase {
return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesUpdateWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): UpdateWalletUseCase {
return UpdateWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@ -124,14 +195,30 @@ internal object WalletsDomainModule {
@Provides
@Singleton
fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase {
return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager)
fun providesGetWalletsSyncUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetWalletNamesUseCase {
return GetWalletNamesUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@Singleton
fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase {
return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager)
fun providesDeleteWalletUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): DeleteWalletUseCase {
return DeleteWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides
@ -214,9 +301,13 @@ internal object WalletsDomainModule {
@Singleton
fun providesGetSavedWalletChangesIdUseCase(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): GetSavedWalletsCountUseCase {
return GetSavedWalletsCountUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}

View file

@ -24,9 +24,7 @@ import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.*
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager(
): CompletionResult<VisaSignedDataByCustomerWallet> {
return runTaskAsyncReturnOnMain(
runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove,
VisaCustomerWalletApproveTask.Input(
cardId = visaDataForApprove.customerWalletCardId,
targetAddress = visaDataForApprove.targetAddress,
hashToSign = visaDataForApprove.dataToSign.hashToSign,
sign = visaDataForApprove.dataToSign::sign,
),
),
cardId = visaDataForApprove.customerWalletCardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),

View file

@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.*
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@ -10,7 +11,6 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
@ -22,15 +22,13 @@ import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
class VisaCustomerWalletApproveTask(
private val visaDataForApprove: VisaDataForApprove,
private val visaDataForApprove: Input,
) : CardSessionRunnable<VisaSignedDataByCustomerWallet> {
override fun run(session: CardSession, callback: CompletionCallback<VisaSignedDataByCustomerWallet>) {
@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask(
return
}
if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) {
if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) {
callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError))
return
}
@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask(
)
}
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
private fun hashPersonalMessage(message: ByteArray): ByteArray {
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
return (prefix + message).toKeccak()
}
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask(
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes()
val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}"
val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val signTask = SignHashCommand(
hash = hashToSign,
hash = hash,
walletPublicKey = targetWalletPublicKey,
derivationPath = derivationPath,
)
@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask(
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
signature = result.data.signature,
hash = hashToSign,
hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask(
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.dataToSign.sign(
signature = rsvSignature,
customerWalletAddress = visaDataForApprove.targetAddress,
),
signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress),
)
}
is CompletionResult.Failure -> {
@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask(
}
}
}
data class Input(
val cardId: String? = null,
val targetAddress: String,
val hashToSign: String,
val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet,
)
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
@ -119,6 +120,8 @@ internal object UserWalletsListManagerModule {
@ApplicationContext applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
passwordRequester: HotWalletPasswordRequester,
appPreferencesStore: AppPreferencesStore,
hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
): UserWalletsListRepository {
val moshi = buildMoshi()
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
@ -162,8 +165,9 @@ internal object UserWalletsListManagerModule {
passwordRequester = passwordRequester,
userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository,
tangemSdkManagerProvider = Provider { tangemSdkManager },
appPreferencesStore = appPreferencesStore,
savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now
// TODO add a settings toggle to disable saving persistent information
hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository,
)
}

View file

@ -8,6 +8,9 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
@ -22,6 +25,8 @@ import com.tangem.domain.core.wallets.error.SetLockError
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
@ -30,10 +35,11 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class DefaultUserWalletsListRepository(
private val publicInformationRepository: UserWalletsPublicInformationRepository,
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
@ -42,6 +48,8 @@ internal class DefaultUserWalletsListRepository(
private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository,
private val tangemSdkManagerProvider: Provider<TangemSdkManager>,
private val savePersistentInformation: ProviderSuspend<Boolean>,
private val appPreferencesStore: AppPreferencesStore,
private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
) : UserWalletsListRepository {
override val userWallets = MutableStateFlow<List<UserWallet>?>(null)
@ -76,7 +84,9 @@ internal class DefaultUserWalletsListRepository(
override suspend fun userWalletsSync(): List<UserWallet> {
load()
return userWallets.value!!
return requireNotNull(userWallets.value) {
"This should never happen"
}
}
override suspend fun selectedUserWalletSync(): UserWallet? {
@ -126,38 +136,46 @@ internal class DefaultUserWalletsListRepository(
userWallet
}
override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either<SetLockError, Unit> =
either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
override suspend fun setLock(
userWalletId: UserWalletId,
lockMethod: LockMethod,
changeUnsecured: Boolean,
): Either<SetLockError, Unit> = either {
val userWallet = userWallets.value?.find { it.walletId == userWalletId }
?: raise(SetLockError.UserWalletNotFound)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
val encryptionKey = userWallet.encryptionKey
?: raise(SetLockError.UserWalletLocked)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
runCatching {
userWalletEncryptionKeysRepository.save(
encryptionKey = UserWalletEncryptionKey(
walletId = userWalletId,
encryptionKey = encryptionKey,
),
removeUnsecured = changeUnsecured,
method = when (lockMethod) {
is LockMethod.AccessCode -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
LockMethod.Biometric -> {
UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric
}
LockMethod.NoLock -> {
if (userWallet is UserWallet.Cold) {
raise(SetLockError.UserWalletNotFound)
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured
}
},
)
}.onFailure { raise(SetLockError.UnableToSetLock(it)) }
}
override suspend fun removeBiometricLock(userWalletId: UserWalletId) {
userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): Either<DeleteWalletError, Unit> = either {
if (userWalletIds.isEmpty()) return Unit.right()
@ -173,6 +191,8 @@ internal class DefaultUserWalletsListRepository(
userWalletEncryptionKeysRepository.delete(userWalletIds)
val userWalletsBeforeDelete = userWallets.value ?: return@either
userWallets.update { currentWallets ->
currentWallets?.filterNot { it.walletId in userWalletIds }
}
@ -181,7 +201,7 @@ internal class DefaultUserWalletsListRepository(
if (currentSelected == null) return@update null
userWallets.value?.findAvailableUserWallet(
userWallets.value?.indexOfFirst { it.walletId == currentSelected.walletId } ?: 0,
userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0,
)
}
}
@ -199,7 +219,7 @@ internal class DefaultUserWalletsListRepository(
when (unlockMethod) {
UserWalletsListRepository.UnlockMethod.Biometric -> {
unlockAllWallets()
unlockAllWallets().bind()
select(userWalletId)
}
UserWalletsListRepository.UnlockMethod.AccessCode -> {
@ -208,6 +228,7 @@ internal class DefaultUserWalletsListRepository(
}
val encryptionKey = requestPasswordRecursive(
hotWalletId = userWallet.hotWalletId,
block = { password ->
runCatching {
userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password)
@ -224,8 +245,10 @@ internal class DefaultUserWalletsListRepository(
return@either
}
removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { userWallets.value?.updateWith(it) }
.doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } }
.doOnFailure { error ->
raise(UnlockWalletError.UnableToUnlock)
}
@ -255,6 +278,7 @@ internal class DefaultUserWalletsListRepository(
}
override suspend fun unlockAllWallets(): Either<UnlockWalletError, Unit> = either {
val userWalletIds = userWalletsSync().map { it.walletId }.toSet()
val biometricKeys = runCatching {
userWalletEncryptionKeysRepository.getAllBiometric()
}.getOrElse {
@ -263,9 +287,28 @@ internal class DefaultUserWalletsListRepository(
}
val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured()
val allKeys = biometricKeys + unsecuredKeys
val allKeys = (biometricKeys + unsecuredKeys).distinct()
val unlockedWalletsIds = allKeys.map { it.walletId }
val unlockedWallets = unlockedWalletsIds.mapNotNull { id ->
userWalletsSync().firstOrNull { it.walletId == id }
}
// Remove all password attempts for unlocked hot wallets
unlockedWallets.forEach {
removePasswordAttempts(it)
}
// if we cant unlock all wallets
if (userWalletIds.all { it in unlockedWalletsIds }.not()) {
raise(UnlockWalletError.UnableToUnlock)
}
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { userWallets.value?.updateWith(it) }
.doOnSuccess { sensitiveInfo ->
userWallets.update { it?.updateWith(sensitiveInfo) }
}
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
}
override suspend fun lockAllWallets(): Either<LockWalletsError, Unit> = either {
@ -293,12 +336,16 @@ internal class DefaultUserWalletsListRepository(
}
private suspend fun requestPasswordRecursive(
hotWalletId: HotWalletId,
block: suspend (CharArray) -> UserWalletEncryptionKey?,
biometryFallback: suspend () -> Either<UnlockWalletError, Unit>,
): Either<UnlockWalletError, UserWalletEncryptionKey?> {
val result = passwordRequester.requestPassword(
hasBiometry = tangemSdkManagerProvider.invoke().needEnrollBiometrics,
val attemptRequest = HotWalletPasswordRequester.AttemptRequest(
hotWalletId = hotWalletId,
authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts
hasBiometry = hasBiometry(),
)
val result = passwordRequester.requestPassword(attemptRequest)
return when (result) {
HotWalletPasswordRequester.Result.Dismiss -> {
@ -309,9 +356,10 @@ internal class DefaultUserWalletsListRepository(
val decrypted = block(result.password.value)
if (decrypted == null) {
passwordRequester.wrongPassword()
requestPasswordRecursive(block, biometryFallback)
requestPasswordRecursive(hotWalletId, block, biometryFallback)
} else {
passwordRequester.successfulAuthentication()
passwordRequester.dismiss()
decrypted.right()
}
}
@ -319,13 +367,28 @@ internal class DefaultUserWalletsListRepository(
biometryFallback()
.onRight {
passwordRequester.successfulAuthentication()
passwordRequester.dismiss()
}
passwordRequester.dismiss()
null.right()
.map { null }
}
}
}
private suspend fun removePasswordAttempts(userWallet: UserWallet) {
if (userWallet is UserWallet.Hot) {
hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId)
}
}
private suspend fun hasBiometry(): Boolean {
val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY,
default = false,
)
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
}
/**
* Find the nearest available wallet that can be selected
*

View file

@ -25,8 +25,14 @@ internal class UserWalletEncryptionKeysRepository(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
suspend fun save(encryptionKey: UserWalletEncryptionKey, method: EncryptionMethod) = withContext(dispatchers.io) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
suspend fun save(
encryptionKey: UserWalletEncryptionKey,
removeUnsecured: Boolean = true,
method: EncryptionMethod,
) = withContext(dispatchers.io) {
if (removeUnsecured) {
secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name)
}
when (method) {
EncryptionMethod.Unsecured -> {
@ -35,12 +41,6 @@ internal class UserWalletEncryptionKeysRepository(
data = encryptionKey.encode(),
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
is EncryptionMethod.Password -> {
val encodedWithPass = AESEncryptionProtocol.encryptWithPassword(
password = method.password,
@ -51,26 +51,37 @@ internal class UserWalletEncryptionKeysRepository(
data = encodedWithPass,
)
}
EncryptionMethod.Biometric -> {
authenticatedStorage.store(
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
}
}
storeUserWalletId(userWalletId = encryptionKey.walletId)
}
fun removeBiometricKey(userWalletId: UserWalletId) {
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
}
suspend fun getAllUnsecured(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
getUserWalletsIds().mapNotNull { userWalletId ->
secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey()
}
}
suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? {
val encrypted = secureStorage.get(
account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name,
) ?: return null
suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? =
withContext(dispatchers.io) {
val encrypted = secureStorage.get(
account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name,
) ?: return@withContext null
val decrypted = AESEncryptionProtocol.decryptWithPassword(password, encrypted)
return decrypted.decodeToKey()
}
withContext(dispatchers.default) {
AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey()
}
}
suspend fun getAllBiometric(): List<UserWalletEncryptionKey> = withContext(dispatchers.io) {
val keys = getUserWalletsIds().map { userWalletId ->

View file

@ -13,9 +13,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.operations.sign.SignHashCommand
@ -42,7 +39,6 @@ import org.json.JSONArray
import org.json.JSONObject
import timber.log.Timber
import java.math.BigDecimal
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
@Suppress("LargeClass")
class WalletConnectSdkHelper {
@ -227,8 +223,6 @@ class WalletConnectSdkHelper {
)
return when (result) {
is Result.Success -> {
val sentFrom = CoreAnalyticsParam.TxSentFrom.WalletConnect
Analytics.send(Basic.TransactionSent(sentFrom = sentFrom, memoType = MemoType.Null))
val hash = result.data.hash
if (hash.startsWith(HEX_PREFIX)) {
hash
@ -394,7 +388,8 @@ class WalletConnectSdkHelper {
signature = signedHash,
hash = hashToSign,
publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().formatHex().lowercase() // use lowercase because some dapps cant handle UPPERCASE
).asRSVLegacyEVM().toHexString().formatHex()
.lowercase() // use lowercase because some dapps cant handle UPPERCASE
}
}
is CompletionResult.Failure -> {

View file

@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule {
wcSessionsRepository: WalletConnectSessionsRepository,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule {
blockchainHelper = TangemWcBlockchainHelper(),
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = coroutineDispatcherProvider,
walletConnectFeatureToggles = walletConnectFeatureToggles,
)

View file

@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.model.legacy.Session
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.extensions.dispatchOnMain
@ -38,18 +37,14 @@ class WalletConnectInteractor(
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val userWalletsListManager: UserWalletsListManager,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
val blockchainHelper: WcBlockchainHelper,
) {
private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled }
private var isWalletConnectReadyForDeepLinks = false
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetSelectedWalletUseCase(userWalletsListManager)
}
private val wcScope = CoroutineScope(
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")

View file

@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -64,6 +66,14 @@ class DetailsMiddleware {
when (action.setting) {
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
AppSetting.RequireAccessCode -> toggleRequireAccessCode(
state = state,
enable = action.enable,
)
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(
state = state,
enable = action.enable,
)
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
@ -90,6 +100,91 @@ class DetailsMiddleware {
}
}
private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.useBiometricAuthentication() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
toggleRequireAccessCode(
state = state,
enable = true,
)
if (enable) {
setBiometricLockForAllWallets()
} else {
// Remove all biometric-related data
removeAllBiometricData()
}
walletsRepository.setUseBiometricAuthentication(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
// Nothing to change
if (walletsRepository.requireAccessCode() == enable) {
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
return@launch
}
if (enable) {
// Remove all biometric sign data
removeAllBiometricSingData()
toggleSaveAccessCodes(state, enable = false)
} else {
toggleSaveAccessCodes(state, enable = true)
}
walletsRepository.setRequireAccessCode(value = enable)
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
}
}
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWalletsListRepository.setLock(
userWalletId = it.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
}
}
private suspend fun removeAllBiometricData() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
userWalletsListRepository.userWalletsSync().forEach {
userWalletsListRepository.removeBiometricLock(it.walletId)
}
removeAllBiometricSingData()
}
private suspend fun removeAllBiometricSingData() {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach {
if (it is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = it.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId),
),
)
}
}
}
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {

View file

@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
saveWallets = true, // User can't enable access codes saving without wallets saving
saveAccessCodes = action.enable,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = true,
requireAccessCode = action.enable,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = true,
useBiometricAuthentication = action.enable,
)
},
)
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy(
@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isInProgress = false,
saveAccessCodes = action.prevState,
)
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
isInProgress = false,
requireAccessCode = action.prevState,
)
AppSetting.BiometricAuthentication -> state.appSettingsState.copy(
isInProgress = false,
needEnrollBiometrics = action.prevState,
)
},
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(

View file

@ -12,9 +12,14 @@ data class DetailsState(
) : StateType
data class AppSettingsState(
@Deprecated("Delete after hot wallet release")
val saveWallets: Boolean = false,
@Deprecated("Delete after hot wallet release")
val saveAccessCodes: Boolean = false,
@Deprecated("Delete after hot wallet release")
val isBiometricsAvailable: Boolean = false,
val requireAccessCode: Boolean = false,
val useBiometricAuthentication: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val isHidingEnabled: Boolean = false,
val isInProgress: Boolean = false,
@ -25,5 +30,5 @@ data class AppSettingsState(
enum class SecurityOption { LongTap, PassCode, AccessCode }
enum class AppSetting {
SaveWallets, SaveAccessCode
SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication,
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory {
onDismiss = onDismiss,
)
}
fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(
R.string.app_settings_off_biometrics_alert_message,
wrappedList(resourceReference(R.string.common_biometrics)),
),
confirmText = resourceReference(R.string.common_disable),
onConfirm = onDisable,
onDismiss = onDismiss,
)
}
fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_on_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_enable),
onConfirm = { onEnable() },
onDismiss = onDismiss,
)
}
fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
return Dialog.Alert(
title = resourceReference(R.string.common_attention),
description = resourceReference(R.string.app_settings_off_require_access_code_alert_message),
confirmText = resourceReference(R.string.common_disable),
onConfirm = { onDisable() },
onDismiss = onDismiss,
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.wallet.R
@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory {
)
}
fun createUseBiometricsSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_USE_BIOMETRICS_SWITCH,
title = resourceReference(R.string.app_settings_enable_biometrics_title),
description = resourceReference(
R.string.app_settings_biometrics_footer,
wrappedList(resourceReference(R.string.common_biometrics)),
),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createRequireAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
onCheckedChange: (Boolean) -> Unit,
): Item.Switch {
return Item.Switch(
id = ID_REQUIRE_ACCESS_CODE_SWITCH,
title = resourceReference(R.string.app_settings_require_access_code),
description = resourceReference(R.string.app_settings_require_access_code_footer),
isEnabled = isEnabled,
isChecked = isChecked,
onCheckedChange = onCheckedChange,
)
}
fun createSaveAccessCodeSwitch(
isChecked: Boolean,
isEnabled: Boolean,
@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory {
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button"
const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button"
const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch"
const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch"
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model(), StoreSubscriber<DetailsState> {
private val itemsFactory = AppSettingsItemsFactory()
@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor(
onClick = ::showAppCurrencySelector,
).let(::add)
if (state.isBiometricsAvailable) {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
onCheckedChange = ::onBiometricAuthenticationToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
).let(::add)
} else {
if (state.isBiometricsAvailable) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
).let(::add)
}
}
itemsFactory.createFlipToHideBalanceSwitch(
@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor(
}
}
private fun onBiometricAuthenticationToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param))
if (isChecked) {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = true)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
} else {
updateContentState {
copy(
dialog = dialogsFactory.createDisableBiometricAuthenticationAlert(
onDisable = {
onSettingsToggled(AppSetting.BiometricAuthentication, enable = false)
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
),
)
}
}
}
private fun onRequireAccessCodeToggled(isChecked: Boolean) {
// TODO : Uncomment and implement analytics event when ready
// val param = AnalyticsParam.OnOffState(isChecked)
// analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param))
updateContentState {
copy(
dialog = if (isChecked) {
dialogsFactory.createEnableRequireAccessCodeAlert(
onEnable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = true)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
} else {
dialogsFactory.createDisableRequireAccessCodeAlert(
onDisable = {
onSettingsToggled(AppSetting.RequireAccessCode, enable = false)
dismissDialog()
},
onDismiss = ::dismissDialog,
)
},
)
}
}
private fun onSaveWalletsToggled(isChecked: Boolean) {
if (isChecked) {
onSettingsToggled(AppSetting.SaveWallets, enable = true)
@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(),
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
requireAccessCode = walletsRepository.requireAccessCode(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,

View file

@ -37,6 +37,9 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId =
callSdk { changeAuth(unlockHotWallet, auth) }
override suspend fun removeBiometryAuthIfPresented(id: HotWalletId): HotWalletId =
callSdk { removeBiometryAuthIfPresented(id) }
override suspend fun derivePublicKey(
unlockHotWallet: UnlockHotWallet,
request: DeriveWalletRequest,

View file

@ -11,13 +11,13 @@ import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler

View file

@ -4,11 +4,16 @@ import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider {
internal class DefaultAuthProvider(
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val useNewListRepository: Boolean = false,
) : AuthProvider {
override fun getCardPublicKey(): String {
val userWallet = userWalletsListManager.selectedUserWalletSync
override suspend fun getCardPublicKey(): String {
val userWallet = getSelectedWallet()
if (userWallet !is UserWallet.Cold) {
return ""
@ -17,8 +22,8 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
return userWallet.scanResponse.card.cardPublicKey.toHexString()
}
override fun getCardId(): String {
val userWallet = userWalletsListManager.selectedUserWalletSync
override suspend fun getCardId(): String {
val userWallet = getSelectedWallet()
if (userWallet !is UserWallet.Cold) {
return ""
@ -27,9 +32,25 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle
return userWallet.scanResponse.card.cardId
}
override fun getCardsPublicKeys(): Map<String, String> {
return userWalletsListManager.userWalletsSync.filterIsInstance<UserWallet.Cold>().associate {
override suspend fun getCardsPublicKeys(): Map<String, String> {
return getWallets().filterIsInstance<UserWallet.Cold>().associate {
it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString()
}
}
private suspend fun getWallets(): List<UserWallet> {
return if (useNewListRepository) {
userWalletsListRepository.userWalletsSync()
} else {
userWalletsListManager.userWalletsSync
}
}
private suspend fun getSelectedWallet(): UserWallet? {
return if (useNewListRepository) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.tap.network.auth.DefaultAppVersionProvider
@ -22,8 +24,16 @@ internal class AuthModule {
@Provides
@Singleton
fun provideAuthProvider(userWalletsListManager: UserWalletsListManager): AuthProvider {
return DefaultAuthProvider(userWalletsListManager)
fun provideAuthProvider(
userWalletsListManager: UserWalletsListManager,
userWalletsListRepository: UserWalletsListRepository,
hotWalletFeatureToggles: HotWalletFeatureToggles,
): AuthProvider {
return DefaultAuthProvider(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
)
}
@Provides

View file

@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
@ -130,6 +131,18 @@ internal class DefaultRampManager(
}
}
override fun checkAssetRequirements(requirements: AssetRequirementsCondition?): Boolean {
return when (requirements) {
AssetRequirementsCondition.PaidTransaction,
is AssetRequirementsCondition.PaidTransactionWithFee,
is AssetRequirementsCondition.RequiredTrustline,
-> false
is AssetRequirementsCondition.IncompleteTransaction,
null,
-> true
}
}
private suspend fun getExchangeableState(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,

View file

@ -21,6 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -31,7 +32,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.operations.attestation.CardArtworksProvider
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
@ -77,4 +80,7 @@ data class DaggerGraphState(
val cardArworksProvider: CardArtworksProvider? = null,
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
val userTokensResponseStore: UserTokensResponseStore? = null,
val userWalletsListRepository: UserWalletsListRepository? = null,
val hotWalletFeatureToggles: HotWalletFeatureToggles? = null,
val tangemHotSdk: TangemHotSdk? = null,
) : StateType

View file

@ -8,6 +8,7 @@ import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.account.AccountCreateEditComponent
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
@ -17,8 +18,10 @@ import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.CreateMobileWalletComponent
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
@ -36,6 +39,7 @@ 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.swap.v2.api.SendWithSwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -52,6 +56,7 @@ import com.tangem.tap.routing.component.RoutingComponent.Child
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent
import com.tangem.features.welcome.WelcomeComponent as NewWelcomeComponent
@ActivityScoped
@Suppress("LongParameterList", "LargeClass")
@ -70,6 +75,7 @@ internal class ChildFactory @Inject constructor(
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
private val stakingComponentFactory: StakingComponent.Factory,
private val swapComponentFactory: SwapComponent.Factory,
@ -90,6 +96,7 @@ internal class ChildFactory @Inject constructor(
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory,
private val accountDetailsComponentFactory: AccountDetailsComponent.Factory,
private val archivedAccountListComponentFactory: ArchivedAccountListComponent.Factory,
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val usedeskComponentFactory: UsedeskComponent.Factory,
@ -98,10 +105,13 @@ internal class ChildFactory @Inject constructor(
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
private val walletActivationComponentFactory: WalletActivationComponent.Factory,
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -138,14 +148,22 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.Welcome -> {
createComponentChild(
context = context,
params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
createComponentChild(
context = context,
params = Unit,
componentFactory = newWelcomeComponentFactory,
)
} else {
createComponentChild(
context = context,
params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
}
}
is AppRoute.WalletSettings -> {
createComponentChild(
@ -393,6 +411,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = PushNotificationsParams(
modelCallbacks = PushNotificationsModelCallbacksStub(),
source = route.source,
nextRoute = AppRoute.Home(),
),
componentFactory = pushNotificationsComponentFactory,
@ -483,6 +502,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletActivationComponentFactory,
)
}
is AppRoute.CreateWalletBackup -> {
createComponentChild(
context = context,
params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = createWalletBackupComponentFactory,
)
}
is AppRoute.UpdateAccessCode -> {
createComponentChild(
context = context,
@ -539,6 +567,22 @@ internal class ChildFactory @Inject constructor(
componentFactory = accountDetailsComponentFactory,
)
}
is AppRoute.ArchivedAccountList -> {
createComponentChild(
context = context,
params = ArchivedAccountListComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = archivedAccountListComponentFactory,
)
}
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(),
componentFactory = tangemPayDetailsComponentFactory,
)
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.common.routing
import android.os.Bundle
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
@ -30,8 +31,10 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Welcome(
@Deprecated("No longer used, will be removed in future releases")
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
// we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware
@Deprecated("No longer used, will be removed in future releases")
val intent: SerializableIntent? = null,
) : AppRoute(path = "/welcome"), RouteBundleParams {
@ -193,7 +196,15 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId")
@Serializable
data object PushNotification : AppRoute(path = "/push_notification")
data class PushNotification(
val source: Source,
) : AppRoute(path = "/push_notification") {
enum class Source {
Stories,
Main,
Onboarding,
}
}
@Serializable
data class WalletSettings(
@ -308,6 +319,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}")
@Serializable
data class CreateWalletBackup(
val userWalletId: UserWalletId,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable
data class UpdateAccessCode(
val userWalletId: UserWalletId,
@ -341,4 +357,12 @@ sealed class AppRoute(val path: String) : Route {
data class AccountDetails(
val account: Account,
) : AppRoute(path = "/account_details/${account.accountId.value}")
@Serializable
data class ArchivedAccountList(
val userWalletId: UserWalletId,
) : AppRoute(path = "/archived_account/${userWalletId.stringValue}")
@Serializable
data object TangemPayDetails : AppRoute(path = "/tangem_pay_details")
}

View file

@ -0,0 +1,134 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
enum class AccountIconSize {
Default, Large, Medium, Small, ExtraSmall
}
/**
* Displays an account icon that can either show a letter (derived from [name])
* or a predefined vector resource (from [icon]).
*
* The background color is determined by the icon's [CryptoPortfolioIconUM.color],
* and the icon size, text style, and box modifier are adapted based on the given [size].
*
* @param name The text reference used to resolve and display the first letter
* when [icon] is set to [CryptoPortfolioIcon.Icon.Letter].
* @param icon The account icon definition, which can be a letter or a drawable resource.
* @param size The size of the icon, defined by [AccountIconSize].
*/
@Composable
fun AccountIcon(
name: TextReference,
icon: CryptoPortfolioIconUM,
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val boxModifier = modifier.selectBoxModifier(size)
val iconSize = Modifier.selectIconSize(size)
val textStyle = when (size) {
AccountIconSize.Default -> TangemTheme.typography.h3
AccountIconSize.Large -> TangemTheme.typography.h1
AccountIconSize.Medium -> TangemTheme.typography.subtitle1
AccountIconSize.Small -> TangemTheme.typography.subtitle2
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
}
Box(
contentAlignment = Alignment.Center,
modifier = boxModifier.background(icon.color.getUiColor()),
) {
val icon = icon.value
val letter = name.resolveReference().firstOrNull()
when {
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
text = letter?.uppercase() ?: "",
style = textStyle,
color = TangemTheme.colors.text.constantWhite,
)
else -> Icon(
modifier = iconSize,
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = icon.getResId()),
contentDescription = null,
)
}
}
}
private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> this.size(20.dp)
AccountIconSize.Large -> this.size(40.dp)
AccountIconSize.Medium -> this.size(16.dp)
AccountIconSize.Small -> this.size(12.dp)
AccountIconSize.ExtraSmall -> this.size(8.dp)
}
private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp))
AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp))
AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp))
AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp))
AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp))
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AccountIcon() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Account Name")
Row(
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall)
}
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall)
}
}
}
object AccountIconPreviewData {
fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM(
value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(),
color = Color.entries.random(),
)
}

View file

@ -0,0 +1,110 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Displays a row representing an account with an icon, title, and subtitle.
*
* The row consists of:
* - An [AccountIcon] on the left.
* - A column with the [title] and [subtitle] texts, which can be displayed in normal
* or reversed order depending on [isReverse].
*
* The layout uses horizontal spacing between the icon and text, and vertical spacing
* between the title and subtitle.
*
* @param title The main text shown in the row, usually representing the account name.
* @param subtitle The secondary text, typically providing additional details about the account.
* @param icon The account icon definition, displayed using [AccountIcon].
* @param isReverse If `true`, the [subtitle] is displayed above the [title].
* Otherwise, the [title] is displayed above the [subtitle].
*/
@Composable
fun AccountRow(
title: TextReference,
subtitle: TextReference,
icon: CryptoPortfolioIconUM,
modifier: Modifier = Modifier,
isReverse: Boolean = false,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AccountIcon(
name = title,
icon = icon,
size = AccountIconSize.Default,
)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
if (isReverse) {
Subtitle(subtitle)
Title(title)
} else {
Title(title)
Subtitle(subtitle)
}
}
}
}
@Composable
private fun Title(title: TextReference) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
}
@Composable
private fun Subtitle(subtitle: TextReference) {
Text(
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
text = subtitle.resolveReference(),
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Main account")
val info = stringReference("10 tokens in 2 networks")
val subtitle = resourceReference(R.string.account_form_name)
fun icon(letter: Boolean = false) = AccountIconPreviewData.randomAccountIcon(letter)
Column(
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
AccountRow(title = name, subtitle = info, icon = icon())
AccountRow(title = name, subtitle = subtitle, icon = icon(), isReverse = true)
}
}

View file

@ -47,4 +47,7 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int {
CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24
CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24
}
}
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color)

View file

@ -1,4 +1,4 @@
package com.tangem.features.account.common
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
@ -12,7 +12,4 @@ data class CryptoPortfolioIconUM(
value = domainModel.value,
color = domainModel.color,
)
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color)
}

View file

@ -60,6 +60,7 @@ class AmountStateConverter(
return AmountState.Data(
title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
availableBalanceShort = stringReference(crypto),
tokenName = stringReference(status.currency.name),
tokenIconState = iconStateConverter.convert(status),
amountTextField = amountFieldConverter.convert(value.value),
@ -130,6 +131,7 @@ class AmountStateConverterV2(
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
},
availableBalanceShort = stringReference(crypto),
tokenName = stringReference(cryptoCurrencyStatus.currency.name),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency),
amountTextField = amountFieldConverter.convert(value.value),

View file

@ -48,6 +48,7 @@ class AmountBoundaryUpdateTransformer(
return prevState.copy(
availableBalance = availableBalance,
availableBalanceShort = stringReference(crypto),
)
}
}

View file

@ -17,7 +17,8 @@ sealed class AmountState {
/**
* @param isPrimaryButtonEnabled indicates if next state button enabled
* @param title title
* @param availableBalance user crypto currency balance
* @param availableBalance user crypto currency balance with fiat balance
* @param availableBalanceShort user crypto currency balance without fiat balance
* @param tokenIconState crypto currency icon state
* @param segmentedButtonConfig currency switcher config
* @param selectedButton selected currency index
@ -33,6 +34,7 @@ sealed class AmountState {
override val isRedesignEnabled: Boolean,
val title: TextReference,
val availableBalance: TextReference,
val availableBalanceShort: TextReference,
val tokenName: TextReference,
val tokenIconState: CurrencyIconState,
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,

View file

@ -24,7 +24,8 @@ object AmountStatePreviewData {
val amountState = AmountState.Data(
isPrimaryButtonEnabled = false,
title = stringReference("Family Wallet"),
availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"),
availableBalance = stringReference("2 130,88 USDT • 2 129,92 \$)"),
availableBalanceShort = stringReference("2 130,88 USDT"),
tokenIconState = CurrencyIconState.Loading,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -25,6 +26,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
import java.math.BigDecimal
@Composable
@ -63,7 +65,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24),
.padding(top = TangemTheme.dimens.spacing24)
.testTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT),
)
Text(
text = secondAmount,
@ -72,7 +75,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
.padding(top = TangemTheme.dimens.spacing8)
.testTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT),
)
}
}

View file

@ -64,7 +64,7 @@ fun AmountBlockV2(
AmountBlockV2(
title = amountState.title,
balance = amountState.availableBalance,
balance = amountState.availableBalanceShort,
currencyTitle = currencyTitle,
currencyIconState = amountState.tokenIconState,
firstAmount = firstAmount,

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendScreenTestTags
import kotlinx.collections.immutable.PersistentList
private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
@ -77,7 +79,8 @@ internal fun LazyListScope.buttons(
.padding(
vertical = TangemTheme.dimens.spacing10,
horizontal = TangemTheme.dimens.spacing34,
),
)
.testTag(StakingSendScreenTestTags.MAX_BUTTON),
)
}
}
@ -90,7 +93,8 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment
.fillMaxSize()
.padding(
horizontal = TangemTheme.dimens.spacing10,
),
)
.testTag(StakingSendScreenTestTags.CURRENCY_BUTTON),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
@ -102,13 +106,13 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment
url = button.iconUrl,
size = TangemTheme.dimens.size18,
isGrayscale = !isSegmentedButtonsEnabled,
modifier = iconModifier,
modifier = iconModifier.testTag(StakingSendScreenTestTags.FIAT_ICON),
)
} else if (button.iconState != null) {
CurrencyIcon(
state = button.iconState,
shouldDisplayNetwork = false,
modifier = iconModifier,
modifier = iconModifier.testTag(StakingSendScreenTestTags.CURRENCY_ICON),
)
}
Text(

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
@ -28,6 +29,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendScreenTestTags
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@ -116,7 +118,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
textAlign = TextAlign.Center,
modifier = Modifier
.align(TopCenter)
.padding(bottom = TangemTheme.dimens.spacing32),
.padding(bottom = TangemTheme.dimens.spacing32)
.testTag(StakingSendScreenTestTags.SECONDARY_AMOUNT),
)
AmountFieldError(
isError = amountField.isError,

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendScreenTestTags
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
@ -52,7 +54,8 @@ internal fun LazyListScope.amountField(
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing14),
.padding(top = TangemTheme.dimens.spacing14)
.testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference()
@ -66,7 +69,8 @@ internal fun LazyListScope.amountField(
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
.padding(top = TangemTheme.dimens.spacing2)
.testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT),
)
}
CurrencyIcon(

View file

@ -2,9 +2,11 @@ package com.tangem.common.ui.amountScreen.utils
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN
import com.tangem.core.ui.format.bigdecimal.approximateAmount
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.utils.StringsSigns.DASH_SIGN
import java.math.BigDecimal
fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
@ -19,12 +21,19 @@ fun getFiatString(
appCurrency: AppCurrency,
approximate: Boolean = false,
): String {
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
if (value == null || rate == null) return DASH_SIGN
val feeValue = value.multiply(rate)
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
withApproximateSign = approximate,
)
return feeValue.format {
if (approximate) {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
).approximateAmount()
} else {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
}
}

View file

@ -15,6 +15,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -22,18 +25,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.buttons.common.contentColor
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import com.tangem.core.ui.utils.singleEvent
import com.tangem.core.ui.test.StakingSendScreenTestTags
@Composable
fun NavigationButtonsBlock(
@ -47,7 +50,7 @@ fun NavigationButtonsBlock(
modifier = modifier.fillMaxWidth(),
) {
InfoText(footerText)
ExtraButtons(state?.extraButtons, state?.txUrl)
DoneButtons(state?.extraButtons)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
@ -58,9 +61,33 @@ fun NavigationButtonsBlock(
}
}
@Composable
fun NavigationButtonsBlockV2(
navigationUM: NavigationUM,
modifier: Modifier = Modifier,
footerText: TextReference? = null,
) {
val navigationUM = navigationUM as? NavigationUM.Content
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth(),
) {
InfoText(footerText)
DoneButtons(navigationUM?.secondaryPairButtonsUM)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
PreviousButton(navigationUM?.prevButton)
NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f))
}
}
}
@Composable
fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
val wrappedButton by rememberNavigationButton(primaryButton)
val hapticFeedback = LocalHapticFeedback.current
AnimatedContent(
targetState = wrappedButton,
transitionSpec = { navigationButtonsTransition() },
@ -83,7 +110,12 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
onClick = {
if (button.isHapticClick) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
button.onClick()
},
showProgress = button.showProgress,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
@ -116,40 +148,47 @@ private fun PreviousButton(prevButton: NavigationButton?) {
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable(onClick = button.onClick)
.padding(TangemTheme.dimens.spacing12),
.padding(TangemTheme.dimens.spacing12)
.testTag(StakingSendScreenTestTags.PREVIOUS_BUTTON),
)
}
}
}
@Composable
private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl: String?) {
fun DoneButtons(pairButtons: Pair<NavigationButton, NavigationButton>?, modifier: Modifier = Modifier) {
AnimatedVisibility(
visible = !txUrl.isNullOrBlank() && extraButtons != null,
visible = pairButtons != null,
enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()),
exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()),
label = "Animate show sent state buttons",
modifier = Modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth(),
) {
val buttons = remember(this) { requireNotNull(extraButtons) }
val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtons) }
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
) {
buttons.forEach { button ->
val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) }
?: TangemButtonIconPosition.None
TangemButton(
text = button.textReference.resolveReference(),
icon = icon,
textStyle = TangemTheme.typography.subtitle1,
onClick = rememberHapticFeedback(state = button, onAction = button.onClick),
modifier = Modifier.weight(1f),
enabled = button.isEnabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
)
}
SecondaryButtonIconStart(
text = leftButton.textReference.resolveReference(),
iconResId = requireNotNull(leftButton.iconRes),
onClick = {
singleEvent {
leftButton.onClick()
}
},
modifier = Modifier.weight(1f),
)
SecondaryButtonIconStart(
text = rightButton.textReference.resolveReference(),
iconResId = requireNotNull(rightButton.iconRes),
onClick = {
singleEvent {
rightButton.onClick()
}
},
modifier = Modifier.weight(1f),
)
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.common.ui.navigationButtons
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
sealed class NavigationButtonsState {
data object Empty : NavigationButtonsState()
@ -10,7 +9,7 @@ sealed class NavigationButtonsState {
data class Data(
val primaryButton: NavigationButton?,
val prevButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>,
val extraButtons: Pair<NavigationButton, NavigationButton>?,
val txUrl: String? = null,
val onTextClick: (String) -> Unit,
) : NavigationButtonsState()

View file

@ -5,29 +5,25 @@ import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import kotlinx.collections.immutable.persistentListOf
internal object NavigationButtonsPreview {
private val extraButtons = persistentListOf(
NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
),
NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
),
private val extraButtons = NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
) to NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
)
private val prev = NavigationButton(

View file

@ -3,7 +3,9 @@ package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -22,6 +24,7 @@ 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 coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.R
@ -184,6 +187,19 @@ fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modi
radius = TangemTheme.dimens.size2,
)
}
is UserWalletItemUM.ImageState.MobileWallet -> {
Image(
modifier = Modifier
.size(36.dp)
.background(
color = TangemTheme.colors.field.focused,
shape = RoundedCornerShape(10.dp),
)
.padding(6.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_mobile_wallet_icon_24),
contentDescription = null,
)
}
is UserWalletItemUM.ImageState.Image -> {
val verifiedArtwork = imageState.artwork.verifiedArtwork
if (verifiedArtwork != null) {
@ -364,6 +380,18 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Failed,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
imageState = UserWalletItemUM.ImageState.MobileWallet,
isEnabled = true,
onClick = {},
),
)
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {

View file

@ -2,8 +2,8 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -35,6 +35,7 @@ class UserWalletItemUMConverter(
private val appCurrency: AppCurrency? = null,
private val balance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val authMode: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
private val artwork: ArtworkModel? = null,
) : Converter<UserWallet, UserWalletItemUM> {
@ -48,24 +49,38 @@ class UserWalletItemUMConverter(
name = stringReference(name),
information = getInfo(userWallet = this),
balance = getBalanceInfo(userWallet = this),
isEnabled = !isLocked,
isEnabled = isEnabled(userWallet = this),
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = artwork?.let {
UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it))
} ?: UserWalletItemUM.ImageState.Loading,
label = if (this is UserWallet.Hot && !this.backedUp) {
LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
)
} else {
null
},
imageState = getImageState(userWallet = value),
label = getLabelOrNull(userWallet = this),
)
}
}
private fun isEnabled(userWallet: UserWallet): Boolean {
return authMode || userWallet.isLocked.not()
}
private fun getLabelOrNull(userWallet: UserWallet): LabelUM? {
return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) {
LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
)
} else {
null
}
}
private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState {
return when {
userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet
artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork))
else -> UserWalletItemUM.ImageState.Loading
}
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) {
is UserWallet.Cold -> {

View file

@ -56,6 +56,8 @@ data class UserWalletItemUM(
data object Loading : ImageState()
data object MobileWallet : ImageState()
data class Image(
val artwork: ArtworkUM,
) : ImageState()

View file

@ -112,7 +112,12 @@ sealed class AnalyticsParam {
val permissionType: String,
) : TxSentFrom("Approve"), TxData
data object WalletConnect : TxSentFrom("WalletConnect")
data class WalletConnect(
override val blockchain: String,
override val token: String,
override val feeType: FeeType?,
) : TxSentFrom("WalletConnect"), TxData
data object Sell : TxSentFrom("Sell")
data class NFT(
@ -131,7 +136,7 @@ sealed class AnalyticsParam {
sealed interface TxData {
val blockchain: String
val token: String
val feeType: FeeType
val feeType: FeeType?
}
sealed class FeeType(val value: String) {

View file

@ -55,7 +55,9 @@ sealed class Basic(
if (sentFrom is AnalyticsParam.TxData) {
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token
this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value
sentFrom.feeType?.value?.let {
this[AnalyticsParam.FEE_TYPE] = it
}
}
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType

View file

@ -9,7 +9,7 @@
},
{
"name": "STAKING_TON_ENABLED",
"version": "undefined"
"version": "5.28.0"
},
{
"name": "NFT_MEDIA_CONTENT_ENABLED",
@ -17,7 +17,7 @@
},
{
"name": "STAKING_CARDANO_ENABLED",
"version": "undefined"
"version": "5.28.0"
},
{
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
@ -33,7 +33,7 @@
},
{
"name": "SEND_VIA_SWAP_ENABLED",
"version": "undefined"
"version": "5.28.0"
},
{
"name": "SWAP_REDESIGN_ENABLED",
@ -41,7 +41,7 @@
},
{
"name": "SEND_REDESIGN_ENABLED",
"version": "undefined"
"version": "5.28.0"
},
{
"name": "WALLET_BALANCE_FETCHER_ENABLED",
@ -52,7 +52,15 @@
"version": "undefined"
},
{
"name": "NEW_TOKEN_RECEIVE_ENABLED",
"name": "NFT_SEND_REDESIGN_ENABLED",
"version": "5.28.0"
},
{
"name": "TANGEM_PAY_ENABLED",
"version": "undefined"
},
{
"name": "NEW_TOKEN_RECEIVE_ENABLED",
"version": "5.28.0"
}
]

View file

@ -14,6 +14,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking
import javax.inject.Singleton
@Module
@ -41,6 +42,12 @@ internal object FeatureTogglesManagerModule {
localTogglesStorage = localTogglesStorage,
versionProvider = versionProvider,
)
}.also {
// We need to initialize during the hilt graph creation
// in order to provide the feature toggles correctly to other dependencies.
runBlocking {
it.init()
}
}
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
@ -24,10 +23,14 @@ internal class DevFeatureTogglesManager(
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
private var featureTogglesMap: MutableMap<String, Boolean>? = null
private var localFeatureTogglesMap: Map<String, Boolean>? = null
override suspend fun init() {
if (featureTogglesMap != null && localFeatureTogglesMap != null) {
return // Already initialized
}
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
@ -46,21 +49,21 @@ internal class DevFeatureTogglesManager(
.toMutableMap()
}
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap!!
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap)
featureTogglesMap!![name] ?: return
featureTogglesMap!![name] = isEnabled
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!)
}
override suspend fun recoverLocalConfig() {
featureTogglesMap = localFeatureTogglesMap.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap)
featureTogglesMap = localFeatureTogglesMap!!.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)

View file

@ -5,7 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in PROD build
@ -18,18 +17,22 @@ internal class ProdFeatureTogglesManager(
private val versionProvider: VersionProvider,
) : FeatureTogglesManager {
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
private var featureToggles: Map<String, Boolean>? = null
override suspend fun init() {
if (featureToggles != null) {
return // Already initialized
}
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
featureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get() ?: "")
}
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false
override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getProdFeatureToggles() = featureToggles
fun getProdFeatureToggles() = featureToggles!!
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun setProdFeatureToggles(map: Map<String, Boolean>) {

View file

@ -77,7 +77,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
mockedImplementation(deps.chucker)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -8,12 +8,12 @@ interface AuthProvider {
/**
* Returns authToken for tangem tech api
*/
fun getCardPublicKey(): String
suspend fun getCardPublicKey(): String
fun getCardId(): String
suspend fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
suspend fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -15,12 +15,14 @@ import okio.IOException
* Switch api environment [Interceptor]
*
* @property id api config id [ApiConfig.ID]
* @property baseUrls base urls for all api config environments
* @property apiConfigsManager api configs manager
*
[REDACTED_AUTHOR]
*/
internal class SwitchEnvironmentInterceptor(
private val id: ApiConfig.ID,
private val baseUrls: Set<String>,
private val apiConfigsManager: ApiConfigsManager,
) : Interceptor {
@ -39,10 +41,13 @@ internal class SwitchEnvironmentInterceptor(
return chain.proceed(request)
}
private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl {
return this.newBuilder()
.host(host = url.toHttpUrl().host)
.build()
private fun HttpUrl.adjustBaseUrl(newBaseUrl: String): HttpUrl {
val currentUrl = this.toString()
val currentBaseUrl = baseUrls.first { currentUrl.contains(it) }
return currentUrl
.replace(oldValue = currentBaseUrl, newValue = newBaseUrl)
.toHttpUrl()
}
private fun Request.Builder.addHeaders(headers: Map<String, ProviderSuspend<String>>): Request.Builder {

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import retrofit2.http.Body
import retrofit2.http.POST
@ -17,5 +18,5 @@ interface BlockAidApi {
suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse
@POST("solana/message/scan")
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse
}

View file

@ -7,7 +7,7 @@ import com.tangem.datasource.api.common.blockaid.models.response.TransactionMeta
@JsonClass(generateAdapter = true)
data class SolanaTransactionScanRequest(
@Json(name = "encoding") val encoding: String = "base64",
@Json(name = "chain") val chain: String,
@Json(name = "blockchain") val blockchain: String,
@Json(name = "method") val method: String,
@Json(name = "options") val options: List<String> = listOf("simulation", "validation"),
@Json(name = "metadata") val metadata: TransactionMetadata,

View file

@ -16,6 +16,7 @@ data class Asset(
@Json(name = "chain_id") val chainId: Int? = null,
@Json(name = "logo_url") val logoUrl: String? = null,
@Json(name = "symbol") val symbol: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "decimals") val decimals: Int? = null,
)

View file

@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Exposure(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: Asset,
@Json(name = "spenders") val spenders: Map<String, SpenderDetails>,
)

View file

@ -0,0 +1,58 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SolanaTransactionResponse(
@Json(name = "result") val result: SolanaTransactionResult,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionResult(
@Json(name = "validation") val validation: SolanaTransactionValidation,
@Json(name = "simulation") val simulation: SolanaTransactionSimulation? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionValidation(
@Json(name = "result_type") val resultType: String,
@Json(name = "description") val description: String?,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionSimulation(
@Json(name = "account_summary") val accountSummary: SolanaTransactionAccountSummary,
@Json(name = "error") val error: String? = null,
@Json(name = "error_details") val errorDetails: String? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionAccountSummary(
@Json(name = "account_assets_diff")
val accountAssetsDiff: List<SolanaTransactionAssetDiff>,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionAssetDiff(
@Json(name = "asset_type") val assetType: String,
@Json(name = "asset") val asset: SolanaTransactionAsset,
@Json(name = "in") val inTransfer: SolanaTransferDetail? = null,
@Json(name = "out") val outTransfer: SolanaTransferDetail? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransactionAsset(
@Json(name = "address") val address: String? = null,
@Json(name = "symbol") val symbol: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "decimals") val decimals: Int? = null,
@Json(name = "type") val type: String? = null,
@Json(name = "logo") val logoUrl: String? = null,
)
@JsonClass(generateAdapter = true)
data class SolanaTransferDetail(
@Json(name = "value") val amount: String? = null,
@Json(name = "summary") val summary: String? = null,
)

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.ProviderSuspend
@ -14,20 +15,44 @@ internal class StakeKit(
private val stakeKitAuthProvider: StakeKitAuthProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey),
"accept" to ProviderSuspend { "application/json" },
),
headers = createHeaders(),
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
}
private fun createHeaders() = buildMap {
put(key = "X-API-KEY", value = ProviderSuspend(stakeKitAuthProvider::getApiKey))
put(key = "accept", value = ProviderSuspend { "application/json" })
}
}

View file

@ -1,19 +1,7 @@
package com.tangem.datasource.api.pay
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest
import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.pay.models.request.*
import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
@ -33,9 +21,17 @@ interface TangemPayApi {
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCustomerWallet(
@Body request: GenerateNonceByCustomerWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNonceByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetTokenByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
@Json(name = "message_format") val messageFormat: String,
)

View file

@ -1,9 +1,11 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.local.config.providers.models.ProviderModel
import retrofit2.http.*
@ -30,9 +32,6 @@ interface TangemTechApi {
@Query("limit") limit: Int? = null,
): ApiResponse<CoinsResponse>
@GET("v1/rates")
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
@GET("v1/currencies")
suspend fun getCurrencyList(
@Header("Cache-Control") cacheControl: String = "max-age=600",
@ -68,46 +67,11 @@ interface TangemTechApi {
@Query("fields") fields: String,
): ApiResponse<QuotesResponse>
@GET("v1/promotion")
suspend fun getPromotionInfo(
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromotionInfoResponse>
@GET("v1/settings/{wallet_id}")
suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse<UserTokensSettingsResponse>
@PUT("v1/settings/{wallet_id}")
suspend fun saveUserTokensSettings(
@Path("wallet_id") walletId: String,
@Body userTokensSettings: UserTokensSettingsResponse,
): ApiResponse<Unit>
@POST("v1/user-network-account")
suspend fun createUserNetworkAccount(
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
@POST("v1/account")
suspend fun createUserTokensAccount(
@Body body: CreateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}")
suspend fun updateUserTokensAccount(
@Path("account_id") accountId: Int,
@Body body: UpdateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}/archive")
suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}/unarchive")
suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@GET("v1/features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
@ReadTimeout(duration = 5, unit = TimeUnit.SECONDS)
@GET("v1/networks/providers")
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
@ -160,7 +124,7 @@ interface TangemTechApi {
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
// endregion
// region wallets
// region user-wallets
@PATCH("v1/user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@ -176,4 +140,20 @@ interface TangemTechApi {
@GET("v1/user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
// region account
@GET("/v1/wallets/{walletId}/accounts")
suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse<GetWalletAccountsResponse>
@PUT("/v1/wallets/{walletId}/accounts")
suspend fun saveWalletAccounts(
@Path("walletId") walletId: String,
@Header("If-Match") ifMatch: String,
): ApiResponse<SaveWalletAccountsResponse>
@GET("/v1/wallets/{walletId}/accounts/archived")
suspend fun getWalletArchivedAccounts(
@Path("walletId") walletId: String,
): ApiResponse<GetWalletArchivedAccountsResponse>
// endregion
}

View file

@ -16,6 +16,7 @@ data class UserTokensResponse(
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String? = null,
@Json(name = "accountId") val accountId: String? = null,
@Json(name = "networkId") val networkId: String,
@Json(name = "derivationPath") val derivationPath: String? = null,
@Json(name = "name") val name: String,

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
@JsonClass(generateAdapter = true)
data class GetWalletAccountsResponse(
@Json(name = "wallet") val wallet: Wallet,
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
@Json(name = "unassignedTokens") val unassignedTokens: List<UserTokensResponse.Token>,
) {
@JsonClass(generateAdapter = true)
data class Wallet(
@Json(name = "version") val version: Int,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetWalletArchivedAccountsResponse(
@Json(name = "archivedAccounts") val accounts: List<WalletAccountDTO>,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SaveWalletAccountsResponse(
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
)

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@JsonClass(generateAdapter = true)
data class WalletAccountDTO(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "derivation") val derivationIndex: Int,
@Json(name = "icon") val icon: String,
@Json(name = "iconColor") val iconColor: String,
@Json(name = "tokens") val tokens: List<UserTokensResponse.Token>? = null,
@Json(name = "totalTokens") val totalTokens: Int? = null,
@Json(name = "totalNetworks") val totalNetworks: Int? = null,
)

View file

@ -7,7 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
@ -42,6 +42,7 @@ import javax.inject.Singleton
*/
@Singleton
internal class RetrofitApiBuilder @Inject constructor(
private val apiConfigs: ApiConfigs,
private val apiConfigsManager: ApiConfigsManager,
@NetworkMoshi private val moshi: Moshi,
private val analyticsErrorHandler: AnalyticsErrorHandler,
@ -49,6 +50,8 @@ internal class RetrofitApiBuilder @Inject constructor(
private val appLogsStore: AppLogsStore,
) {
private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls()
/**
* Builds a Retrofit API instance for the specified API configuration ID
*
@ -95,13 +98,26 @@ internal class RetrofitApiBuilder @Inject constructor(
val writeTimeoutSeconds: Long? = null,
)
private fun getConfigsBaseUrls(): Map<ApiConfig.ID, Set<String>> {
return apiConfigs.associate { config ->
val allBaseUrls = config.environmentConfigs.mapTo(hashSetOf(), ApiEnvironmentConfig::baseUrl)
config.id to allBaseUrls
}
}
private fun OkHttpClient.Builder.applyApiConfig(
apiConfigId: ApiConfig.ID,
environmentConfig: ApiEnvironmentConfig,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) {
return if (BuildConfig.TESTER_MENU_ENABLED) {
addInterceptor(
interceptor = SwitchEnvironmentInterceptor(id = apiConfigId, apiConfigsManager = apiConfigsManager),
interceptor = SwitchEnvironmentInterceptor(
id = apiConfigId,
baseUrls = configsBaseUrls[apiConfigId]
?: error("Base URLs for ApiConfig with id [$apiConfigId] not found"),
apiConfigsManager = apiConfigsManager,
),
)
} else {
val headers = environmentConfig.headers

View file

@ -84,6 +84,10 @@ object PreferencesKeys {
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") }
val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") }
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
@ -165,6 +169,18 @@ object PreferencesKeys {
fun getShouldShowInitialPermissionScreen(permission: String) =
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
// endregion
// region Hot Wallet unlock attempts
fun getHotWalletUnlockAttemptsKey(attemptId: String) =
intPreferencesKey(name = "hotWalletUnlockAttempts_$attemptId")
fun getHotWalletUnlockBootKey(attemptId: String) = intPreferencesKey(name = "hotWalletUnlockBootCount_$attemptId")
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
// endregion
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -5,6 +5,10 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
@Deprecated(
message = "Use UserWalletsListRepository instead",
replaceWith = ReplaceWith("UserWalletsListRepository"),
)
interface UserWalletsStore {
val selectedUserWalletOrNull: UserWallet?
@ -15,8 +19,6 @@ interface UserWalletsStore {
fun getSyncStrict(key: UserWalletId): UserWallet
suspend fun getAllSyncOrNull(): List<UserWallet>?
suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,

View file

@ -18,6 +18,7 @@ import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
@ -55,8 +56,8 @@ internal class ProdApiConfigsManagerTest {
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
every { appInfoProvider.osVersion } returns "Android 16"
}

View file

@ -116,8 +116,10 @@
<string name="cardano_max_amount_has_token_title">Nicht genug ADA</string>
<string name="common_accept">Akzeptieren</string>
<string name="common_access_denied">Zugang verweigert</string>
<string name="common_add">Hinzufügen</string>
<string name="common_add_to_portfolio">Zum Portfolio hinzufügen</string>
<string name="common_add_token">Token hinzufügen</string>
<string name="common_address">Vertragsadresse</string>
<string name="common_all">Alle</string>
<string name="common_allow">Erlauben</string>
<string name="common_amount">Betrag</string>
@ -946,9 +948,13 @@
<string name="send_validation_invalid_fee">Die Gebühr geht über die Bilanz hinaus</string>
<string name="send_validation_invalid_total">Der Gesamtbetrag geht über die Bilanz hinaus</string>
<string name="send_with_swap_confirm_title">Tauschen und senden</string>
<string name="send_with_swap_convert_token_alert_message">Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht.</string>
<string name="send_with_swap_correct_recipient_network_notification_message">Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust.</string>
<string name="send_with_swap_correct_recipient_network_notification_title">Wähle das richtige Empfängernetzwerk</string>
<string name="send_with_swap_notification_text">Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht nahtlos.</string>
<string name="send_with_swap_recipient_amount_text">Wird an den Empfänger gesendet</string>
<string name="send_with_swap_recipient_amount_title">Zu erhaltender Betrag</string>
<string name="send_with_swap_remove_convert_alert_message">Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht.</string>
<string name="send_with_swap_title">Senden mit Swap</string>
<string name="sent_transaction_sent_title">Transaktion gesendet</string>
<string name="settings_card_settings_footer">Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest.</string>
@ -1391,23 +1397,32 @@
<string name="warning_token_trustline_button_title">Trustline aktivieren</string>
<string name="warning_token_trustline_subtitle">Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s.</string>
<string name="warning_token_trustline_title">Trustline erforderlich</string>
<string name="wc_alert_add_network_to_portfolio_description">Das erforderliche Netzwerk %s ist nicht in Ihrem Portfolio hinzugefügt. Fügen Sie es zuerst hinzu und fahren Sie dann mit der Verbindung fort.</string>
<string name="wc_alert_add_network_to_portfolio_title">Netzwerk zum Portfolio hinzufügen</string>
<string name="wc_alert_audit_malicious_domain">Bösartige/ verdächtige Domäne</string>
<string name="wc_alert_audit_unknown_domain">Unbekannte Domäne</string>
<string name="wc_alert_connect_anyway">Trotzdem verbinden</string>
<string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string>
<string name="wc_alert_connection_timeout_title">WalletConnect konnte nicht hergestellt werden</string>
<string name="wc_alert_domain_issues_description">Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann.</string>
<string name="wc_alert_network_not_connected_description">Um fortzufahren, verbinden Sie bitte Ihre dApp-Sitzung erneut mit dem erforderlichen Netzwerk %s.</string>
<string name="wc_alert_network_not_connected_title">Netzwerk nicht verbunden</string>
<string name="wc_alert_request_timeout_description">Überprüfen Sie Ihre Netzwerkverbindung</string>
<string name="wc_alert_request_timeout_title">Anforderungs-Zeitüberschreitung</string>
<string name="wc_alert_session_disconnected_description">Bitte kehre zu Deinem Browser zurück und stellen die Verbindung über WalletConnect erneut her.</string>
<string name="wc_alert_session_disconnected_title">WalletConnect-Sitzung wurde getrennt</string>
<string name="wc_alert_sign_anyway">Trotzdem unterschreiben</string>
<string name="wc_alert_unknown_error_description">Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unknown_error_description_no_error_code">Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unknown_error_title">Wir haben einen unbekannten Fehler festgestellt.</string>
<string name="wc_alert_unsupported_dapps_description">Tangem Wallet unterstützt derzeit nicht %s</string>
<string name="wc_alert_unsupported_method_description">Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unsupported_method_title">Wir haben einen unbekannten Fehler festgestellt.</string>
<string name="wc_alert_unsupported_network_description">Dieses Netzwerk %s wird von Tangem Wallet nicht unterstützt und kann nicht verbunden werden.</string>
<string name="wc_alert_unsupported_network_title">Nicht unterstütztes Netzwerk</string>
<string name="wc_alert_unsupported_networks_description">Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht.</string>
<string name="wc_alert_unsupported_networks_title">Nicht unterstützte Netzwerke</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s</string>
<string name="wc_alert_verified_domain_description">Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. </string>
<string name="wc_alert_verified_domain_title">Verifizierte Domain</string>
<string name="wc_alert_wrong_card_description">Falsche Karte oder falscher Ring in der App ausgewählt</string>
<string name="wc_alert_wrong_card_title">Wir haben eine Art Problem</string>
@ -1431,7 +1446,7 @@
<string name="wc_copy_data_button_text">Daten kopieren</string>
<string name="wc_custom_allowance_title">Benutzerdefinierter Freibetrag</string>
<string name="wc_disconnect_all">Alle trennen</string>
<string name="wc_disconnect_all_alert_desc">Text über die Trennung aller dApps</string>
<string name="wc_disconnect_all_alert_desc">Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein.</string>
<string name="wc_disconnect_all_alert_title">Alle dApps trennen</string>
<string name="wc_errors_invalid_domain_subtitle">Versuchen Sie erneut, mit einer neuen URI zu koppeln</string>
<string name="wc_errors_invalid_domain_title">Ungültige dApp-Domain</string>
@ -1455,7 +1470,7 @@
<string name="wc_transaction_request">Transaktionsanfrage</string>
<string name="wc_transaction_request_title">Transaktionsanfrage</string>
<string name="wc_unlimited_amount">Unbegrenzte Menge</string>
<string name="wc_wallet_connect">Wallet verbinden</string>
<string name="wc_wallet_connect">WalletConnect</string>
<string name="welcome_interrupted_backup_alert_discard">Verwerfen</string>
<string name="welcome_interrupted_backup_alert_message">Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen?</string>
<string name="welcome_interrupted_backup_alert_resume">Ja, fortsetzen</string>

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