Updated on 2026-08-14
This commit is contained in:
commit
e4754fed3b
483 changed files with 9315 additions and 4084 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule {
|
|||
ApiConfig.ID.TangemTech,
|
||||
ApiConfig.ID.Express,
|
||||
ApiConfig.ID.TangemPay,
|
||||
ApiConfig.ID.StakeKit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
379
app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt
Normal file
379
app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -191,6 +191,15 @@ internal object TransactionDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePrepareAndSignUseCase(
|
||||
transactionRepository: TransactionRepository,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): PrepareAndSignUseCase {
|
||||
return PrepareAndSignUseCase(transactionRepository, cardSdkConfigRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSignUseCase(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class AmountBoundaryUpdateTransformer(
|
|||
|
||||
return prevState.copy(
|
||||
availableBalance = availableBalance,
|
||||
availableBalanceShort = stringReference(crypto),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ fun AmountBlockV2(
|
|||
|
||||
AmountBlockV2(
|
||||
title = amountState.title,
|
||||
balance = amountState.availableBalance,
|
||||
balance = amountState.availableBalanceShort,
|
||||
currencyTitle = currencyTitle,
|
||||
currencyIconState = amountState.tokenIconState,
|
||||
firstAmount = firstAmount,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ data class UserWalletItemUM(
|
|||
|
||||
data object Loading : ImageState()
|
||||
|
||||
data object MobileWallet : ImageState()
|
||||
|
||||
data class Image(
|
||||
val artwork: ArtworkUM,
|
||||
) : ImageState()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ dependencies {
|
|||
|
||||
/** Chucker */
|
||||
debugImplementation(deps.chucker)
|
||||
mockedImplementation(deps.chuckerStub)
|
||||
mockedImplementation(deps.chucker)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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" })
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue