diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a94d7fc4b1..17ee83e2e2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt index 0b0f1a32d0..a8d1a2c038 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt @@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule { ApiConfig.ID.TangemTech, ApiConfig.ID.Express, ApiConfig.ID.TangemPay, + ApiConfig.ID.StakeKit, ) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt new file mode 100644 index 0000000000..7e118e51b1 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt new file mode 100644 index 0000000000..79b38d7c9c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt new file mode 100644 index 0000000000..f71c9ad210 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt @@ -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(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) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 3ba4c711b5..dd53bc1dc5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -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 } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index e6bb7ddeb4..128a24ad03 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -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(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) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index 62365124a9..f553c1cbe0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -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) { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 44cab70be3..76d8e34a7a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -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() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt new file mode 100644 index 0000000000..083478f475 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt @@ -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() } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 23aae9e349..7d225a195e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 23aae9e3496d89a021ac9a0833b54b49635bb193 +Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ed9d07c577..aa3d4f2a9a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index e51fc573ea..f6b3c93141 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 57d5d8c6ca..e3d0a18067 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -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()) } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index fa9d503de0..9cb6b6cd62 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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 @@ -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( diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ef784f8347..09c556cae1 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 8f14501047..805090e6c0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 15530c62b3..f944a89717 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -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 { + 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, diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index dea8dc75a4..d780f3d15a 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -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? { - return userWalletsListManager.userWallets.firstOrNull() - } - override suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt new file mode 100644 index 0000000000..c5275bf034 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt @@ -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> + 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 { + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt index b2a04f9d52..2fe50e3705 100644 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -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) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 17e09eb709..8cf8edf8d6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 0aa400353e..a674e4ac97 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 7be9d95b9a..5f8a03b7d2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -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, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 1bb8a1ec1c..1989f4868e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index cf5e9e0af4..bdbe9037cc 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8b0266061b..0b8802198c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -191,6 +191,15 @@ internal object TransactionDomainModule { ) } + @Provides + @Singleton + fun providePrepareAndSignUseCase( + transactionRepository: TransactionRepository, + cardSdkConfigRepository: CardSdkConfigRepository, + ): PrepareAndSignUseCase { + return PrepareAndSignUseCase(transactionRepository, cardSdkConfigRepository) + } + @Provides @Singleton fun provideSignUseCase( diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 87b107936a..44dd702fc3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index aaf4f87888..09a9039974 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -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 { 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)), diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index edca4bea1a..1568e7dfc8 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index a44a04e051..4fa773c18c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -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 { override fun run(session: CardSession, callback: CompletionCallback) { @@ -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, ) { - 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, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index f6323a20de..c521709443 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index d60a2a0f74..93810c48da 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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, private val savePersistentInformation: ProviderSuspend, + private val appPreferencesStore: AppPreferencesStore, + private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -76,7 +84,9 @@ internal class DefaultUserWalletsListRepository( override suspend fun userWalletsSync(): List { 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 = - either { - val userWallet = userWallets.value?.find { it.walletId == userWalletId } - ?: raise(SetLockError.UserWalletNotFound) + override suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean, + ): Either = 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): Either = 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 = 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 = either { @@ -293,12 +336,16 @@ internal class DefaultUserWalletsListRepository( } private suspend fun requestPasswordRecursive( + hotWalletId: HotWalletId, block: suspend (CharArray) -> UserWalletEncryptionKey?, biometryFallback: suspend () -> Either, ): Either { - 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 * diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index fb42970c29..104cfc0945 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -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 = 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 = withContext(dispatchers.io) { val keys = getUserWalletsIds().map { userWalletId -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index ffb1dbb2b0..9df63ef08a 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 6992564847..6c9e9f189d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -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, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index f602ed8fc4..395ad23885 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -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") diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 3aece96fa9..9281cbc507 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a6f2b28868..e783c18e37 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index bc7f17a7b6..980cacb286 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -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, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 43e66b065a..73459bc7b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 47bbb8376f..bde3cf116f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index ee93849f31..8b9f0e5a01 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -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 { 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, diff --git a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt index 9cc8db4191..af00ff8af8 100644 --- a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt +++ b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 37690ee484..8a842072d8 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 5ce4ad3c20..99425e0191 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -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 { - return userWalletsListManager.userWalletsSync.filterIsInstance().associate { + override suspend fun getCardsPublicKeys(): Map { + return getWallets().filterIsInstance().associate { it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() } } + + private suspend fun getWallets(): List { + return if (useNewListRepository) { + userWalletsListRepository.userWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } + } + + private suspend fun getSelectedWallet(): UserWallet? { + return if (useNewListRepository) { + userWalletsListRepository.selectedUserWalletSync() + } else { + userWalletsListManager.selectedUserWalletSync + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 5f724624af..95004c11d8 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index bfeee05b09..6c4c20e0cc 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 1c696ac842..e0b2794648 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 49f217bf18..615b1c3778 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index e0d6d80408..d0ffcca403 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -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") } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt new file mode 100644 index 0000000000..8a441922f5 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt @@ -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(), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt new file mode 100644 index 0000000000..c48fa40072 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt @@ -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) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt index 14015c86b2..d97401c0a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt @@ -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 } -} \ No newline at end of file +} + +fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this) +fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt similarity index 66% rename from features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt rename to common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt index 299fb679dc..cb8b2d22af 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt @@ -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) \ No newline at end of file +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index ac0ebecf2f..2b60066ea7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -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), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index 89c1074a49..7385fa9542 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -48,6 +48,7 @@ class AmountBoundaryUpdateTransformer( return prevState.copy( availableBalance = availableBalance, + availableBalanceShort = stringReference(crypto), ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index cc23669f47..cac4628783 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -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, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 99c11a9a14..a2db25c4dd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -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( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index e4f345ca11..47418a9338 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -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), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index ee080d579f..979a1a4419 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -64,7 +64,7 @@ fun AmountBlockV2( AmountBlockV2( title = amountState.title, - balance = amountState.availableBalance, + balance = amountState.availableBalanceShort, currencyTitle = currencyTitle, currencyIconState = amountState.tokenIconState, firstAmount = firstAmount, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index f1d8e921a8..753a0fc99e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -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( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 35d49a5387..f491629420 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -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, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 9c31705709..ea0473e930 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -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( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index 9ee08290a8..af9627c4a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -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, + ) + } + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index b612670c59..b22f5341e4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -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?, txUrl: String?) { +fun DoneButtons(pairButtons: Pair?, 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), + ) } } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index e0bddfab66..772c493cdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -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, + val extraButtons: Pair?, val txUrl: String? = null, val onTextClick: (String) -> Unit, ) : NavigationButtonsState() diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 9d2e71fe85..c409bb5b95 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -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( diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 97a5063ec8..31c66358b4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -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 { @@ -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 -> { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index e9f98863b8..c0abb2b485 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -56,6 +56,8 @@ data class UserWalletItemUM( data object Loading : ImageState() + data object MobileWallet : ImageState() + data class Image( val artwork: ArtworkUM, ) : ImageState() diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 79c0d883c0..5879b1920e 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -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) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 110391778a..875cf24182 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -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 diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 1158354430..bd1f7e6e67 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -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" } ] diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index 8346378b52..fa5c04f6a1 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -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() + } } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 7aca4511ea..ce0d035439 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -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 by Delegates.notNull() - private var localFeatureTogglesMap: Map by Delegates.notNull() + private var featureTogglesMap: MutableMap? = null + private var localFeatureTogglesMap: Map? = null override suspend fun init() { + if (featureTogglesMap != null && localFeatureTogglesMap != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( @@ -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 = featureTogglesMap + override fun getFeatureToggles(): Map = 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) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index d723297a45..54fe7a011d 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -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 by Delegates.notNull() + private var featureToggles: Map? = 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) { diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 0bd12e57fd..323153c119 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -77,7 +77,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index 7945fc59eb..da1b1aca5f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -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 + suspend fun getCardsPublicKeys(): Map } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt index 1b2a53ec3c..ee59740bff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt @@ -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, 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>): Request.Builder { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt index 605bc8a391..cdb6ec04e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt @@ -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 } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt index 232343cf53..e89428b6a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt @@ -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 = listOf("simulation", "validation"), @Json(name = "metadata") val metadata: TransactionMetadata, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt index be70cd4a52..a3c5d878a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt @@ -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, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt index e776056d4c..cfab51b716 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt @@ -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, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt new file mode 100644 index 0000000000..d98f56ee5d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt @@ -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, +) + +@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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt index 0efb5068d1..8f0331ca8b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt @@ -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 = 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" }) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 2749826753..8678e1959c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -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 + @POST("v1/auth/challenge") + suspend fun generateNonceByCustomerWallet( + @Body request: GenerateNonceByCustomerWalletRequest, + ): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse + @POST("v1/auth/token") + suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt new file mode 100644 index 0000000000..edd8260545 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt new file mode 100644 index 0000000000..9a5e47e327 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 14ec9f906f..51fd1ce19a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -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 - @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 - @GET("v1/promotion") - suspend fun getPromotionInfo( - @Query("programName") name: String, - @Header("Cache-Control") cacheControl: String = "max-age=600", - ): ApiResponse - - @GET("v1/settings/{wallet_id}") - suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse - - @PUT("v1/settings/{wallet_id}") - suspend fun saveUserTokensSettings( - @Path("wallet_id") walletId: String, - @Body userTokensSettings: UserTokensSettingsResponse, - ): ApiResponse - @POST("v1/user-network-account") suspend fun createUserNetworkAccount( @Body body: CreateUserNetworkAccountBody, ): ApiResponse - @POST("v1/account") - suspend fun createUserTokensAccount( - @Body body: CreateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}") - suspend fun updateUserTokensAccount( - @Path("account_id") accountId: Int, - @Body body: UpdateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}/archive") - suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @PUT("v1/account/{account_id}/unarchive") - suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @GET("v1/features") - suspend fun getFeatures(): ApiResponse - @ReadTimeout(duration = 5, unit = TimeUnit.SECONDS) @GET("v1/networks/providers") suspend fun getBlockchainProviders(): Map> @@ -160,7 +124,7 @@ interface TangemTechApi { suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse // 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 @@ -176,4 +140,20 @@ interface TangemTechApi { @GET("v1/user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion + + // region account + @GET("/v1/wallets/{walletId}/accounts") + suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse + + @PUT("/v1/wallets/{walletId}/accounts") + suspend fun saveWalletAccounts( + @Path("walletId") walletId: String, + @Header("If-Match") ifMatch: String, + ): ApiResponse + + @GET("/v1/wallets/{walletId}/accounts/archived") + suspend fun getWalletArchivedAccounts( + @Path("walletId") walletId: String, + ): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 2476bf0a31..8e1dec299f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -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, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt new file mode 100644 index 0000000000..6c3afd812f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -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, + @Json(name = "unassignedTokens") val unassignedTokens: List, +) { + + @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, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt new file mode 100644 index 0000000000..3f1db4c76b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt new file mode 100644 index 0000000000..3f36276519 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt new file mode 100644 index 0000000000..343b6cdf2a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt @@ -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? = null, + @Json(name = "totalTokens") val totalTokens: Int? = null, + @Json(name = "totalNetworks") val totalNetworks: Int? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt index 87de60fbd0..aa2adc0817 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -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> = 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> { + 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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index a737416dd5..06a5f51102 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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 */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 4f1d6b5cba..b4d50f01de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -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? - suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index defd5db207..61d25e29b1 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -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" } diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7bc007eb36..ddcad5ed92 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -116,8 +116,10 @@ Nicht genug ADA Akzeptieren Zugang verweigert + Hinzufügen Zum Portfolio hinzufügen Token hinzufügen + Vertragsadresse Alle Erlauben Betrag @@ -946,9 +948,13 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Tauschen und senden + Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht. + Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust. + Wähle das richtige Empfängernetzwerk Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. Wird an den Empfänger gesendet Zu erhaltender Betrag + Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht. Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. @@ -1391,23 +1397,32 @@ Trustline aktivieren Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s. Trustline erforderlich + 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. + Netzwerk zum Portfolio hinzufügen Bösartige/ verdächtige Domäne Unbekannte Domäne Trotzdem verbinden Zeitüberschreitungsfehler. Bitte versuche es später erneut. WalletConnect konnte nicht hergestellt werden Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann. + Um fortzufahren, verbinden Sie bitte Ihre dApp-Sitzung erneut mit dem erforderlichen Netzwerk %s. + Netzwerk nicht verbunden + Überprüfen Sie Ihre Netzwerkverbindung + Anforderungs-Zeitüberschreitung Bitte kehre zu Deinem Browser zurück und stellen die Verbindung über WalletConnect erneut her. WalletConnect-Sitzung wurde getrennt Trotzdem unterschreiben Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. + Tangem Wallet unterstützt derzeit nicht %s Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. + Dieses Netzwerk %s wird von Tangem Wallet nicht unterstützt und kann nicht verbunden werden. + Nicht unterstütztes Netzwerk Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s + Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem @@ -1431,7 +1446,7 @@ Daten kopieren Benutzerdefinierter Freibetrag Alle trennen - Text über die Trennung aller dApps + Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein. Alle dApps trennen Versuchen Sie erneut, mit einer neuen URI zu koppeln Ungültige dApp-Domain @@ -1455,7 +1470,7 @@ Transaktionsanfrage Transaktionsanfrage Unbegrenzte Menge - Wallet verbinden + WalletConnect Verwerfen Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? Ja, fortsetzen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 8d38231daf..fa2411cdf5 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -113,8 +113,10 @@ ADA insuficiente Aceptar Acceso denegado + Agregar Añadir al portafolio Agregar token + Dirección Todos Autorizar Montante @@ -1335,12 +1337,18 @@ Habilitar línea de confianza Una línea de confianza debe estar habilitada para recibir este token. La red requiere un %1$s %2$s reserva. Se requiere línea de confianza + La red requerida %s no está añadida a su portafolio. Añádala primero y luego continúe con la conexión. + Agregar red al portafolio Dominio malicioso Dominio desconocido Conectarse de todas formas Error de tiempo de espera. Por favor, inténtalo de nuevo más tarde. Error al establecer WalletConnect Este dominio no puede ser verificado. Compruebe cuidadosamente la solicitud de aprobación. + Para continuar, vuelva a conectar su sesión de dApp con la red requerida %s. + Red no conectada + Verifique su conexión de red + Tiempo de espera de la solicitud agotado Vuelva a su navegador y vuelva a conectarse a través de WalletConnect. La sesión de Wallet Connect se desconectó Firmar de todos modos @@ -1351,9 +1359,11 @@ dApp no compatible Código de error: 8 005. Si el problema persiste, no dudes en contactar con nuestro soporte. Hemos encontrado un error desconocido + Esta red %s no es compatible con Tangem Wallet y no puede conectarse. + Red no compatible Actualmente, Tangem no es compatible con una red requerida por %s. Redes no compatibles - Tangem soporta una red requerida por %s + Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app Tenemos algún tipo de problema @@ -1381,7 +1391,7 @@ Asignación personalizada dApp desconectada Desconectar todo - Texto sobre desconexión de todas las dApps + Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp. Desconectar todas las dApps Intente emparejar nuevamente con una URI nueva Dominio de dApp no válido @@ -1417,7 +1427,7 @@ Cantidad ilimitada Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único URI ya utilizado - Wallet connect + WalletConnect Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 00b0250a97..e907d8df01 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -95,8 +95,10 @@ ADA insuffisant Accepter Accès refusé + Ajouter Ajouter au portfolio Ajouter un jeton + Adresse Tous Permettre Montant @@ -194,6 +196,7 @@ Rejeter Recharger Renommer + Obligatoire Enregistrez Sauvegarder les modifications Rechercher @@ -1316,23 +1319,60 @@ Activer Trustline Une Trustline doit être activée pour recevoir ce jeton. Le réseau requiert une réserve de %1$s %2$s Trustline requise + Le réseau requis %s n’est pas ajouté à votre portefeuille. Ajoutez-le d’abord, puis poursuivez la connexion. + Ajouter le réseau au portefeuille Domaine malveillant + Domaine inconnu + Se connecter quand même + Erreur de délai d\'attente. Veuillez réessayer plus tard. + Échec de la connexion à WalletConnect + Ce domaine ne peut pas être vérifié. Vérifiez attentivement la demande avant de l\'approuver. + Pour continuer, veuillez reconnecter votre session dApp avec le réseau requis %s. + Réseau non connecté + Vérifiez votre connexion réseau + Délai d’attente de la requête dépassé + Veuillez retourner à votre navigateur et vous reconnecter via WalletConnect. + La session Wallet Connect a été déconnectée. Signer quand même + Code d\'erreur : %s. Si le problème persiste, n\'hésitez pas à contacter notre service d\'assistance. Si le problème persiste, n’hésitez pas à contacter notre support. + Nous avons rencontré une erreur inconnue. Le portefeuille Tangem ne prend actuellement pas en charge %ss dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue + Le réseau %s n’est pas pris en charge par Tangem Wallet et ne peut pas être connecté. + Réseau non pris en charge + Tangem ne prend actuellement pas en charge le réseau requis par %s. + Réseaux non pris en charge + Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes. + Domaine vérifié + Carte ou bague incorrecte sélectionnée dans l\'application + Nous avons un problème. + Toutes les dApps sont déconnectées Autoriser à dépenser Adresse + Connecter Chargement + Réseau + Réseaux Illimité + Wallet + Application connectée Réseaux connectés + Connecté à %1$s + Consultez le solde et l\'activité de votre portefeuille + Signer des transactions sans vous en informer + Demander l\'accord pour les transactions + Ne pourra pas + Souhaite + Demande de connexion Connexions Contenu Copier les données + dApp déconnectée Déconnecter tout - Texte sur la déconnexion de toutes les dApps + Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. Déconnecter toutes les dApps Essayez de jumeler à nouveau avec un nouvel URI Domaine dApp invalide @@ -1342,17 +1382,27 @@ La proposition de connexion a expiré Modifications estimées du portefeuille La transaction n\'a pas pu être simulée. Veuillez procéder avec prudence. + Rechargez votre solde pour couvrir les frais de réseau. + %1$s Insuffisant Transaction malveillante + Ajoutez le réseau %s à votre portefeuille pour ce wallet + Le wallet ne nécessite aucun réseaux Nouvelle connexion Connectez votre portefeuille à différentes dApps Aucune séance Des risques potentiels ou un comportement malveillant ont été détectés. Se connecter ou signer des transactions peut entraîner une perte de fonds. + Risque de sécurité connu + Ouvrez l\'application Web3 et sélectionnez l\'option WalletConnect. Demande de Type de signature + Au moins un réseau est requis pour la connexion à une dApp. + Spécifier les réseaux sélectionnés + Signé avec succès À Demande de transaction Demande de transaction Montant illimité + WalletConnect Ignorer Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? Oui, reprendre diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index ff32b8bf8b..2e061c0bce 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,10 +12,13 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + 回復する + アーカイブ済み アカウントをアーカイブする アーカイブ このアカウントをアーカイブしますが、いつでも復元できます。 アカウント + アカウント番号%s — アドレス導出に使用されます。 アカウントを追加 保存 アカウント名 @@ -68,8 +71,10 @@ 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 + 後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。 これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 + これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。 アクセスコードを要求する このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。 アクセスコードを保存 @@ -144,8 +149,10 @@ ADAが不足しています。 受け入れる アクセスが拒否されました + 追加 ポートフォリオに追加 トークンを追加 + アドレス すべて 許可する 金額 @@ -286,6 +293,7 @@ 取引状況 取引 送金 + データを読み込めません… わかりました エラーが発生しました。もう一度お試しください。 アクセスできません @@ -470,6 +478,8 @@ 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップに保存されている既存のウォレットを復元する Googleドライブのバックアップ + Tangemの業界最高水準のハードウェアウォレットで、今すぐセキュリティをアップグレードしましょう。 + ハードウェアウォレット バックアップへ移動 アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。 まずバックアップを完了する @@ -481,7 +491,21 @@ 最新の機能とニュースをお届けします シードフレーズのバックアップ モバイルウォレットを作成する + このリカバリーフレーズはすでにインポートされています。 モバイルウォレット + 手続き中も資金は安全に保管され、完全にアクセス可能です + 資金へのアクセス + すべてのプライベートウォレットデータはモバイルアプリから削除され、Tangemデバイスにのみ安全に保存されます。 + セキュリティ全般 + 秘密鍵は、アプリからTangemカード・リングに移動します + 鍵の移行 + デバイスをスキャン + アップグレードを開始 + ウォレットをTangemウォレットにアップグレードします。これにより、コールドストレージで資産を安全に保管できます。 + Tangemウォレット + ハードウェアウォレットにアップグレード + Tangemの業界最高水準のハードウェアウォレットで、暗号資産を安全に保管しましょう。 + ハードウェアバックアップでウォレットをアップグレード この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -983,10 +1007,13 @@ スワップして送信 変換を続行しますか? これにより以前のデータは消去されます。 変換を確定 + その他の通貨を送信すると、取り返しのつかない損失が発生します。 + 正しい受信者ネットワークを選択してください トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります 受取人へ 受取金額 + 受信者は%sを取得します 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 変換を削除 スワップして送信 @@ -1429,12 +1456,18 @@ トラストラインを有効にする このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。 トラストラインが必要 + 必要なネットワーク %s はポートフォリオに追加されていません。まず追加してから接続を続行してください。 + ネットワークをポートフォリオに追加 悪意のあるドメイン 不明なドメイン とにかく接続する タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。 WalletConnectを確立できませんでした このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 + 続行するには、必要なネットワーク %s でdAppセッションを再接続してください。 + ネットワークが接続されていません + ネットワーク接続を確認してください + リクエストがタイムアウトしました ブラウザに戻り、WalletConnect経由で再接続してください。 Wallet Connectセッションが接続解除されました とにかくサインする @@ -1445,9 +1478,11 @@ サポートされていないdApp エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました + このネットワーク %s はTangem Walletでサポートされておらず、接続できません。 + サポートされていないネットワーク Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク - Tangemは%sで必要なネットワークをサポートします + このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています @@ -1475,7 +1510,7 @@ 使用可能量の設定 dAppが接続解除されました すべての接続を解除する - すべてのdAppsの接続解除に関するテキスト + すべてのdAppセッションの接続が解除されます。ウォレットはどのdAppにもリンクされなくなります。 すべてのdAppを接続解除する 新しいURIで、再度ペアリングを試してください 無効なdAppドメイン @@ -1511,7 +1546,7 @@ 無制限 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています - ウォレットコネクト + WalletConnect 不審な取引 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5b238275be..befe0fd2d7 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -92,8 +92,10 @@ Недостаточно ADA Принять Доступ запрещен + Добавить Добавить в портфель Добавить токен + Адрес Все Разрешить Сумма @@ -230,6 +232,7 @@ Статус транзакции Транзакции Перевод + Невозможно загрузить данные… Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно @@ -1304,24 +1307,32 @@ Открыть Trustline Чтобы получить этот токен, необходимо включить Trustline. Сеть требует резерв %1$s %2$s. Требуется трастлайн + Требуемая сеть %s не добавлена в ваш портфель. Добавьте её, а затем выполните подключение. + Добавьте сеть в ваш портфель Вредоносный домен Неизвестный домен Всё равно подключиться Ошибка тайм-аута. Пожалуйста, попробуйте позже. Не удалось подключиться через Wallet Connect Этот домен не может быть верифицирован. Внимательно проверьте запрос перед одобрением. + Чтобы продолжить, переподключите сессию dApp с требуемой сетью %s. + Сеть не подключена + Проверьте подключение к сети + Время ожидания запроса истекло Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect. Сессия Wallet Connect была завершена Подписать всё равно Код ошибки: %s. Если проблема сохраняется, обратитесь в нашу службу поддержки. Если проблема сохраняется, обратитесь в нашу службу поддержки Мы обнаружили неизвестную ошибку - Кошелек Tangem.в настоящий момент не поддерживает %s + Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp Мы обнаружили неизвестную ошибку + Эта сеть %s не поддерживается Tangem Wallet и не может быть подключена. + Неподдерживаемая сеть Tangem в настоящее время не поддерживает необходимую сеть для %s Неподдерживаемые сети - Tangem поддерживает сеть, необходимую для %s + Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема @@ -1349,6 +1360,7 @@ Настраиваемый лимит dApp отключен Отключить все + Все сессии dApp будут отключены. Ваш кошелёк больше не будет связан ни с одним dApp. Отключить все dApp Попробуйте соединиться снова, используя новый URI Недействительный домен dApp @@ -1356,7 +1368,7 @@ Нет сетей Пожалуйста, сгенерируйте новый URI и попробуйте подключиться снова Предложение подключения истекло - Предварительные изменения + Прогнозируемые изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. Оценка не поддерживается для %s Предложено %s @@ -1383,7 +1395,7 @@ Безлимитное количество Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. URI уже используется - Подключение кошелька + WalletConnect Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 79d1d3c092..5c7d9f4ef4 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -92,8 +92,10 @@ Недостатньо ADA Прийняти Доступ заборонено + Додати Додати у портфель Додати токен + Адреса Усе Дозволити Сума @@ -1292,18 +1294,27 @@ Відкрити Trustline Щоб отримати цей токен, потрібно увімкнути Trustline. Мережа вимагає резерв %1$s %2$s. Відкрийте Trustline + Потрібна мережа %s не додана до вашого портфеля. Спочатку додайте її, а потім виконайте підключення. + Додати мережу до портфеля Невідомий домен Все одно підключитися Помилка тайм-ауту. Будь ласка, спробуйте пізніше. Не вдалося зʼєднатися через Wallet Connect Цей домен не може бути підтверджений. Уважно перевірте запит перед схваленням. + Щоб продовжити, перепідключіть сесію dApp із потрібною мережею %s. + Мережа не підключена + Перевірте підключення до мережі + Час очікування запиту вичерпано Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. Сеанс Wallet Connect було завершено Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою + Tangem Wallet наразі не підтримує %s + Ця мережа %s не підтримується Tangem Wallet і не може бути підключена. + Непідтримувана мережа Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі - Tangem підтримує мережу, необхідну для %s + Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. Верифікований домен Обрана не вірна картка або кільце Схоже, виникла проблема @@ -1327,7 +1338,7 @@ Копіювати дані dApp відключено Розʼєднати все - Відключити всі dApps + Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. Відключити всі dApps Спробуйте ще раз з новим URI Недійсний домен dApp @@ -1335,6 +1346,7 @@ Немає мереж Будь ласка, згенеруйте новий URI та спробуйте ще раз Термін для з’єднання минув + Прогнозовані зміни Поповніть баланс, щоб покрити комісію мережі Недостатньо %1$s Додайте %s мережі до вашого портфелю для цього гаманця @@ -1353,7 +1365,7 @@ До Запит транзакції Запит транзакції - Підключення гаманця + WalletConnect Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4089de8ca8..dc9ced8f5c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Archive You are archiving this account, but you can always get it back. Account + Account #%s — used for address derivation. Add account Save Account name @@ -70,8 +71,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. + You’ll be asked for your wallet’s access code later so we can securely store it for future use This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. Require Access Code This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction. Save Access Code @@ -149,8 +152,10 @@ Not enough ADA Accept Access denied + Add Add to portfolio Add token + Address All Allow Amount @@ -295,6 +300,7 @@ Transaction status Transactions Transfer + Unable to load the data… I understand There was an error. Please try again. Unreachable @@ -479,6 +485,8 @@ If you do, you\'ll need to start over. Recover an existing wallet stored in your Google Drive backup Google Drive Backup + Upgrade your security right away with a best in class hardware wallet from Tangem. + Hardware Wallet Go to backup To secure your wallet with a Access Code, complete the backup first. Finish Backup First @@ -490,7 +498,21 @@ Stay up to date with the latest features and news Seed phrase backup Create Mobile Wallet + This recovery phrase has already been imported Mobile Wallet + Your funds stay safe and fully accessible during the process + Funds access + All private wallet data will be removed from the mobile app and stored securely on your Tangem device only + General Security + Private keys will be moved from the app to your Tangem card or ring + Key Migration + Scan device + Start upgrade + You’re about to upgrade your wallet to Tangem Wallet. This will keep your assets safe with cold storage. + Tangem Wallet + Upgrade to Hardware Wallet + Keep your crypto safe with Tangem’s best-in-class hardware wallet. + Upgrade wallet with a hardware
backup This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation @@ -1001,6 +1023,8 @@ Swap and send Proceed with conversion? This will clear your previous data. Confirm Conversion + Sending any other currency will result in its irreversible loss. + Select the correct recipient network Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive To recipient @@ -1497,12 +1521,18 @@ Enable Trustline A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. Trustline Required + The required network %s is not added to your portfolio. Add it first, then proceed with the connection. + Add network to portfolio Malicious domain Unknown domain Connect anyway Timeout error. Please, try again later. Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. + To continue, please reconnect your dApp session with the required network %s. + Network not connected + Check your network connection + Request timeout Please return to your browser and reconnect via WalletConnect. Wallet Connect session was disconnected Sign anyway @@ -1513,9 +1543,11 @@ Unsupported dApp Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error + This network %s is not supported by Tangem Wallet and cannot be connected. + Unsupported network Tangem does not currently support a required network by %s. Unsupported networks - Tangem support a required network by %s + This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem @@ -1543,7 +1575,7 @@ Custom allowance dApp disconnected Disconnect all - Text about discnected all dApps + All dApp sessions will be disconnected. Your wallet will no longer be linked to any dApps. Disconect All dApps Try pairing again with a fresh URI Invalid dApp domain @@ -1580,7 +1612,7 @@ Unlimited Amount Ensure that each pairing attempt uses a fresh and unique URI URI already used - Wallet connect + WalletConnect Suspicious transaction Discard You have an interrupted backup. Do you want to resume? diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt new file mode 100644 index 0000000000..d355f8ff22 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.components.bottomsheets + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Generic options bottom sheet component + * + * @param config Bottom sheet configuration containing OptionsBottomSheetContent + * @param title Title text for the bottom sheet + * @param containerColor Background color of the bottom sheet + */ +@Composable +fun OptionsBottomSheet( + config: TangemBottomSheetConfig, + title: TextReference, + containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary, +) { + TangemBottomSheet( + config = config, + titleText = title, + containerColor = containerColor, + content = { content -> + OptionsBottomSheetContent(content = content) + }, + ) +} + +@Composable +private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.options.forEachIndexed { index, option -> + InputRowDefault( + text = option.label, + showDivider = index < content.options.size - 1, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = content.options.size - 1, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(option.key) }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OptionsBottomSheetPreview() { + TangemThemePreview { + OptionsBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = "option1", + label = TextReference.Str("First Option"), + ), + BottomSheetOption( + key = "option2", + label = TextReference.Str("Second Option"), + ), + BottomSheetOption( + key = "option3", + label = TextReference.Str("Third Option"), + ), + ), + onOptionClick = {}, + ), + ), + title = TextReference.Str("Select Option"), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt new file mode 100644 index 0000000000..02d3ab9479 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.components.bottomsheets + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param key Unique identifier for the option + * @param label Display text for the option + */ +data class BottomSheetOption( + val key: String, + val label: TextReference, +) + +/** + * @param options List of options to display + * @param onOptionClick Callback when an option is clicked, receives the option key + */ +data class OptionsBottomSheetContent( + val options: ImmutableList = persistentListOf(), + val onOptionClick: (String) -> Unit = {}, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt index 39a042f707..c0d2dac09e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt @@ -3,10 +3,11 @@ package com.tangem.core.ui.components.buttons.small import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -41,20 +42,19 @@ fun TangemIconButton( background: Color = TangemTheme.colors.button.secondary, iconTint: Color = TangemTheme.colors.icon.secondary, ) { - IconButton( - onClick = onClick, + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), + contentDescription = "", + tint = iconTint, modifier = modifier + .size(24.dp) .clip(shape) .background(background) - .size(24.dp), - ) { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), - contentDescription = "", - tint = iconTint, - modifier = Modifier.size(16.dp), - ) - } + .padding(4.dp) + .clickable( + onClick = onClick, + ), + ) } // region Preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index e4d65b95dd..2219e2642d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation @@ -23,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -102,7 +104,9 @@ fun AmountTextField( singleLine = true, readOnly = !isEnabled, visualTransformation = visualTransformation, - modifier = Modifier.background(backgroundColor), + modifier = Modifier + .background(backgroundColor) + .testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index fa00fb9959..214da97109 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields import androidx.compose.animation.* import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -36,9 +37,9 @@ fun PinTextField( value: String, length: Int, isPasswordVisual: Boolean, + pinTextColor: PinTextColor, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, - wrongCode: Boolean = false, ) { val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { @@ -58,7 +59,7 @@ fun PinTextField( } .focusRequester(focusRequester), keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, + keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done, ), singleLine = true, @@ -72,7 +73,7 @@ fun PinTextField( CellDecoration( length = length, isPasswordVisual = isPasswordVisual, - wrongCode = wrongCode, + pinTextColor = pinTextColor, value = value, ) }, @@ -84,17 +85,25 @@ fun PinTextField( } } -@Suppress("MagicNumber") +enum class PinTextColor { + Primary, + WrongCode, + Success, +} + +@Suppress("MagicNumber", "LongMethod") @Composable private fun CellDecoration( length: Int, - wrongCode: Boolean, + pinTextColor: PinTextColor, value: String, modifier: Modifier = Modifier, isPasswordVisual: Boolean = false, ) { val textMeasurer = rememberTextMeasurer() - val width = textMeasurer.measure("0") + val minSize = textMeasurer.measure("0") + val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell + val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell Row( modifier = modifier, @@ -107,6 +116,18 @@ private fun CellDecoration( "" } + val color = when (pinTextColor) { + PinTextColor.Primary -> { + if (isPasswordVisual) { + TangemTheme.colors.icon.informative + } else { + TangemTheme.colors.text.primary1 + } + } + PinTextColor.WrongCode -> TangemTheme.colors.icon.warning + PinTextColor.Success -> TangemTheme.colors.icon.accent + } + Box( modifier = Modifier .background( @@ -119,26 +140,34 @@ private fun CellDecoration( targetState = char, transitionSpec = { ( - fadeIn(animationSpec = tween(220, delayMillis = 90)) + - slideInVertically(animationSpec = tween(330, delayMillis = 0)) + fadeIn(animationSpec = tween(90, delayMillis = 90)) + + slideInVertically(animationSpec = tween(220, delayMillis = 0)) ) .togetherWith( fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)), ) }, ) { text -> - Text( - modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), - text = text, - style = TangemTheme.typography.h3, - color = if (wrongCode) { - TangemTheme.colors.text.warning - } else { - TangemTheme.colors.text.primary1 - }, - textAlign = TextAlign.Center, - lineHeight = 48.sp, - ) + if (isPasswordVisual && text.isNotEmpty()) { + Canvas( + Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + ) { + drawCircle( + color = color, + radius = 4.dp.toPx(), + center = center, + ) + } + } else { + Text( + modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + text = text, + style = TangemTheme.typography.h3, + color = color, + textAlign = TextAlign.Center, + lineHeight = 48.sp, + ) + } } } } @@ -152,10 +181,18 @@ private fun Preview() { var text by remember { mutableStateOf("123") } Column { + PinTextField( + value = text, + onValueChange = { text = it }, + isPasswordVisual = true, + pinTextColor = PinTextColor.Success, + length = 6, + ) PinTextField( value = text, onValueChange = { text = it }, isPasswordVisual = false, + pinTextColor = PinTextColor.Primary, length = 6, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 2d0c1b4fb1..72111e3c58 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -27,7 +27,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.SwapTokenScreenTestTags +import com.tangem.core.ui.test.BaseBlockTestTags /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -67,20 +67,23 @@ fun InputRowDefault( Column( modifier = Modifier .weight(1f) - .testTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK), + .testTag(BaseBlockTestTags.BLOCK), ) { title?.let { Text( text = title.resolveReference(), style = TangemTheme.typography.subtitle2, color = titleColor, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing8) + .testTag(BaseBlockTestTags.BLOCK_TITLE), ) } Text( text = text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, + modifier = Modifier.testTag(BaseBlockTestTags.BLOCK_TEXT), ) } iconRes?.let { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index 418341d870..259553da0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -76,7 +76,7 @@ fun InputRowRecipient( val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning } else { - title to TangemTheme.colors.text.secondary + title to TangemTheme.colors.text.tertiary } DividerContainer( modifier = modifier, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 52bdac7eab..204f816c64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.utils.StringsSigns.DASH_SIGN /** * Market price block @@ -120,7 +120,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { ) } } else { - Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) + Price(price = DASH_SIGN, modifier = priceModifier) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt index 0370272dcc..ace47189b0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -51,7 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { ConstraintLayout( modifier = modifier - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = providerChooseUM.isSelected) .clickable( enabled = !providerChooseUM.hasError(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index 942e60deaf..9de699245f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +24,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags @Suppress("LongParameterList") @Composable @@ -54,7 +56,8 @@ fun RoundableCornersRow( .padding( horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12, - ), + ) + .testTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { @@ -63,6 +66,7 @@ fun RoundableCornersRow( color = startTextColor, maxLines = 1, style = startTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_NAME), ) if (iconResId != null && iconClick != null) { Icon( @@ -85,6 +89,7 @@ fun RoundableCornersRow( color = endTextColor, maxLines = 1, style = endTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_VALUE), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 1499aaecaf..be5201fa5c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -2,7 +2,6 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,7 +22,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags @@ -31,7 +30,7 @@ import com.tangem.utils.StringsSigns @Composable fun SelectorRowItem( - @StringRes titleRes: Int, + title: TextReference, @DrawableRes iconRes: Int, modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12), @@ -80,7 +79,7 @@ fun SelectorRowItem( contentDescription = null, ) Text( - text = stringResourceSafe(titleRes), + text = title.resolveReference(), style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), @@ -151,7 +150,7 @@ private fun RowScope.SelectorValueContent( private fun SelectorRowItemPreview() { TangemThemePreview { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_slow, + title = resourceReference(R.string.common_fee_selector_option_slow), iconRes = R.drawable.ic_tortoise_24, preDot = TextReference.Str("1000 ETH"), postDot = TextReference.Str("1000 $"), diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt deleted file mode 100644 index 2dc5bcd17e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.core.ui.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import timber.log.Timber - -/** - * The ViewModel is scoped to the parent route Navigation graph - * and is provided using the Hilt-generated ViewModel factory - * - * ``` - * val navController = rememberNavController() - * - * navigation( - * route = "parent", - * startDestination = "parent/1" - * ) { - * composable("route/1") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/2") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/3") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * } - * ``` - * - * @param navController NavController within the common NavGraph - * @throws Exception if there is no parent route - */ -@Composable -inline fun NavBackStackEntry.parentHiltViewModel(navController: NavController): T { - val viewModelStoreOwner = remember(this) { - try { - navController.getBackStackEntry(this.destination.parent!!.id) - } catch (e: Exception) { - Timber.tag("scopedViewModel").e(e, "There is no parent route'") - throw e - } - } - - return hiltViewModel(viewModelStoreOwner) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt deleted file mode 100644 index e58126708e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.core.ui.extensions - -import android.R -import android.content.Context -import android.graphics.Color.* -import android.view.WindowManager -import androidx.annotation.ColorRes -import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat -import androidx.fragment.app.Fragment -import kotlin.math.sqrt - -@Deprecated("Use only in legacy fragments") -fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) { - with(requireActivity().window) { - clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - statusBarColor = ContextCompat.getColor(requireContext(), colorResId) - val view = view ?: return - val windowInsetsController = WindowCompat.getInsetsController(this, view) - windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId) - } -} - -// TODO replace by android.graphics.luminance() after bump min API to 24 -@Suppress("MagicNumber") -fun luminance(context: Context, @ColorRes colorRes: Int): Boolean { - val color = context.resources.getColor(colorRes, null) - if (R.color.transparent == color) return true - var rtnValue = false - val rgb = intArrayOf(red(color), green(color), blue(color)) - val brightness = sqrt( - rgb[0] * rgb[0] * .241 + - rgb[1] * rgb[1] * .691 + - rgb[2] * rgb[2] * .068, - ).toInt() - - // color is light - if (brightness >= 200) { - rtnValue = true - } - return rtnValue -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index ef94c6b681..cab83ed0d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -72,26 +71,24 @@ fun Modifier.conditionalCompose( fun Modifier.selectedBorder( isSelected: Boolean, width: Dp = 2.5.dp, - color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + color: Color = TangemTheme.colors.text.accent, radius: Dp = 16.dp, ) = conditionalCompose( condition = isSelected, modifier = { - border( + outsetBorder( width = width, - color = color, - shape = RoundedCornerShape(radius), + color = color.copy(alpha = 0.15f), + shape = RoundedCornerShape(radius + 2.dp), ) - .padding(width) .border( width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(radius - 2.dp), + color = color, + shape = RoundedCornerShape(radius), ) - .clip(RoundedCornerShape(radius - 2.dp)) + .clip(RoundedCornerShape(radius)) }, otherModifier = { - padding(width) - .clip(RoundedCornerShape(radius - 2.dp)) + clip(RoundedCornerShape(radius)) }, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 1834e10f45..aef02f2fe3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -121,7 +121,10 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { +/** + * Returns amount with correct scale + */ +fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt deleted file mode 100644 index c5c4285292..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.core.ui.screen - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.FloatRange -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Modifier -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemTheme - -/** - * An abstract base class for bottom sheet dialogs that use Compose for UI rendering. - * Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { - - /** - * The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED]. - */ - open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED - - /** - * The fraction of the screen height that the bottom sheet should take when expanded. - * Default is `null`, indicating that the height will be determined by the content. - */ - @FloatRange(from = 0.0, to = 1.0) - open val expandedHeightFraction: Float? = null - - override val screenModifier: Modifier - @Composable - @ReadOnlyComposable - get() = Modifier - .fillMaxWidth() - .let { - if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it - } - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ) - - override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView( - context = inflater.context, - activity = requireActivity(), - overrideSystemBarColors = false, - ) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) - - (dialog as BottomSheetDialog).behavior.apply { - state = initialBottomSheetState - skipCollapsed = true - } - - return dialog - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt deleted file mode 100644 index 48565c82d7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.core.ui.screen - -import android.content.res.Configuration -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import com.tangem.core.ui.R - -/** - * An abstract base class for fragments that use Compose for UI rendering. - * Extends [Fragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeFragment : Fragment(), ComposeScreen { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions() - - return createComposeView(inflater.context, requireActivity()).also { - it.isTransitionGroup = isTransitionsInflated - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - /* - * We need to manually dispatch configuration changes to the Compose view. - * - - * `android:configChanges="uiMode"` is set in the manifest. - * */ - view?.dispatchConfigurationChanged(newConfig) - } - - /** - * Inflates transitions for the fragment. Override this method to customize - * enter and exit transitions for the fragment. - * - * @return `true` if transitions were inflated; `false` otherwise. - */ - protected open fun TransitionInflater.inflateTransitions(): Boolean { - enterTransition = inflateTransition(R.transition.fade) - exitTransition = inflateTransition(R.transition.fade) - - return true - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt new file mode 100644 index 0000000000..b2e4eb5d56 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object BaseBlockTestTags { + const val BLOCK = "BASE_BLOCK" + const val BLOCK_TITLE = "BASE_BLOCK_TITLE" + const val BLOCK_TEXT = "BASE_BLOCK_REWARDS_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt new file mode 100644 index 0000000000..f368755c2f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.test + +object StakingDetailsScreenTestTags { + const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + + const val BANNER_IMAGE = "TOKEN_DETAILS_SCREEN_BANNER_IMAGE" + const val BANNER_TEXT = "TOKEN_DETAILS_SCREEN_BANNER_TEXT" + + const val PARAMETER_BLOCK = "STAKING_DETAILS_PARAMETER_BLOCK" + const val PARAMETER_NAME = "STAKING_DETAILS_PARAMETER_NAME" + const val PARAMETER_VALUE = "STAKING_DETAILS_PARAMETER_VALUE" + const val TOS_TEXT = "STAKING_DETAILS_TOS_TEXT" + + const val ACTIVE_STAKING_BLOCK = "STAKING_DETAILS_ACTIVE_STAKING_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt new file mode 100644 index 0000000000..138b27e35b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.test + +object StakingSendDetailsScreenTestTags { + + const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" + const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" + + const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK" + const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt new file mode 100644 index 0000000000..1dc36d3f79 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object StakingSendScreenTestTags { + const val SCREEN_CONTAINER = "STAKING_SEND_SCREEN_CONTAINER" + + const val AMOUNT_CONTAINER_TITLE = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TITLE" + const val AMOUNT_CONTAINER_TEXT = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TEXT" + const val INPUT_TEXT_FIELD = "STAKING_SEND_SCREEN_INPUT_TEXT_FIELD" + const val SECONDARY_AMOUNT = "STAKING_SEND_SCREEN_SECONDARY_AMOUNT" + + const val CURRENCY_BUTTON = "STAKING_SEND_SCREEN_CURRENCY_BUTTON" + const val FIAT_ICON = "STAKING_SEND_SCREEN_FIAT_ICON" + const val CURRENCY_ICON = "STAKING_SEND_SCREEN_CURRENCY_ICON" + const val MAX_BUTTON = "STAKING_SEND_SCREEN_MAX_BUTTON" + const val PREVIOUS_BUTTON = "STAKING_SEND_SCREEN_PREVIOUS_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt index d8be9a8e43..cd6cf947d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -6,7 +6,6 @@ object SwapTokenScreenTestTags { const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" - const val NETWORK_FEE_BLOCK = "SWAP_TOKEN_SCREEN_NETWORK_FEE_BLOCK" const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index f337c0bbc9..bcbee0ae13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -2,7 +2,19 @@ package com.tangem.core.ui.test object TokenDetailsScreenTestTags { const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE" const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON" const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS" + + const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK" + const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK" + const val STAKING_CURRENCY_ICON = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_CURRENCY_ICON" + const val STAKING_SERVICE_TITLE = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TITLE" + const val STAKING_SERVICE_TEXT = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TEXT" + const val STAKING_FIAT_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_FIAT_AMOUNT" + const val STAKING_DOT = "TOKEN_DETAILS_SCREEN_STAKING_DOT" + const val STAKING_TOKEN_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_TOKEN_AMOUNT" + const val STAKING_REWARD_VALUE = "TOKEN_DETAILS_SCREEN_STAKING_REWARD_VALUE" + const val STAKING_CHEVRON_ICON = "TOKEN_DETAILS_SCREEN_STAKING_CHEVRON_ICON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt deleted file mode 100644 index 744a2d9bf3..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.core.ui.utils - -import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.LOWER_SIGN -import com.tangem.utils.StringsSigns.TILDE_SIGN -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Currency -import java.util.Locale - -@Suppress("LargeClass") -@Deprecated("Use BigDecimal.format") -object BigDecimalFormatter { - - const val EMPTY_BALANCE_SIGN = DASH_SIGN - private const val CAN_BE_LOWER_SIGN = LOWER_SIGN - - private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") - - private const val FIAT_MARKET_DEFAULT_DIGITS = 2 - private const val FIAT_MARKET_EXTENDED_DIGITS = 6 - private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 - - private val usdCurrency = Currency.getInstance("USD") - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmount( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - decimals: Int = FIAT_MARKET_DEFAULT_DIGITS, - locale: Locale = Locale.getDefault(), - withApproximateSign: Boolean = false, - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - - val formatterCurrency = getCurrency(fiatCurrencyCode) - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = decimals - minimumFractionDigits = decimals - roundingMode = RoundingMode.HALF_UP - } - - return if (fiatAmount.checkFiatThreshold()) { - buildString { - append(CAN_BE_LOWER_SIGN) - append( - formatter.format(FIAT_FORMAT_THRESHOLD) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), - ) - } - } else { - val formattedAmount = formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - - if (withApproximateSign) { - buildString { - append(TILDE_SIGN) - append(formattedAmount) - } - } else { - formattedAmount - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmountUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val digits = if (fiatAmount.checkFiatThreshold()) { - FIAT_MARKET_EXTENDED_DIGITS - } else { - FIAT_MARKET_DEFAULT_DIGITS - } - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = digits - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatPriceUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val (formattedAmount, finalScale) = getFiatPriceUncappedWithScale(value = fiatAmount) - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = finalScale - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(formattedAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { - return if (value < BigDecimal.ONE) { - val leadingZeroes = value.scale() - value.precision() - val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES - - val amount = value - .setScale(scale, RoundingMode.HALF_UP) - .stripTrailingZeros() - - amount to amount.scale() - } else { - value to FIAT_MARKET_DEFAULT_DIGITS - } - } - - private fun getCurrency(code: String): Currency { - return runCatching { Currency.getInstance(code) } - .getOrElse { e -> - // Currency code is not valid ISO 4217 code - if (e is IllegalArgumentException) { - usdCurrency - } else { - throw e - } - } - } - - private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml new file mode 100644 index 0000000000..101239eb7d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml new file mode 100644 index 0000000000..b610863fbc --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml new file mode 100644 index 0000000000..ecf6f9754c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/ui/src/main/res/drawable/img_approvale_new_24.xml b/core/ui/src/main/res/drawable/img_approvale_new_24.xml new file mode 100644 index 0000000000..bc1f814f54 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_approvale_new_24.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 723da17196..a9fba30f57 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -13,6 +13,8 @@ import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /** [REDACTED_AUTHOR] @@ -37,23 +39,52 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun getArchivedAccount(accountId: AccountId): Option = option { - ArchivedAccount( - accountId = accountId, - name = AccountName("Archived Account").getOrNull()!!, - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = DerivationIndex(value = 1000).getOrNull()!!, - tokensCount = 2, - networksCount = 1, + createMockArchivedAccount(userWalletId = accountId.userWalletId) + } + + override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> = option { + listOf( + createMockArchivedAccount(userWalletId), ) } + override fun getArchivedAccounts(userWalletId: UserWalletId): Flow> { + return flow { + getArchivedAccountsSync(userWalletId).getOrNull().orEmpty() + } + } + + override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit + override suspend fun saveAccounts(accountList: AccountList) { runtimeStore.update(emptyList()) { it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } } } + override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int { + val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1 + + return activeAccountsCount + 1 + } + override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } + + private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount { + val derivationIndex = DerivationIndex(value = 1000).getOrNull()!! + + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + tokensCount = 2, + networksCount = 1, + ) + } } \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index 128d1cfd56..5334f1e066 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -3,7 +3,7 @@ package com.tangem.data.blockaid import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.transaction.* import com.domain.blockaid.models.transaction.simultation.AmountInfo -import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.domain.blockaid.models.transaction.simultation.TokenInfo import com.tangem.blockchain.extensions.hexToBigDecimal @@ -17,6 +17,9 @@ private const val SUCCESS_STATUS = "Success" private const val DOMAIN_CHECKED_STATUS = "hit" private const val VALIDATION_SAFE_STATUS = "Benign" private const val VALIDATION_WARNING_STATUS = "Warning" +private const val VALIDATION_MALICIOUS_STATUS = "Malicious" + +private const val SOL_ASSET_SYMBOL = "SOL" internal object BlockAidMapper { @@ -28,6 +31,26 @@ internal object BlockAidMapper { } } + fun mapToDomain(from: SolanaTransactionResponse): CheckTransactionResult { + val validation = when (from.result.validation.resultType) { + VALIDATION_SAFE_STATUS -> ValidationResult.SAFE + VALIDATION_WARNING_STATUS -> ValidationResult.WARNING + VALIDATION_MALICIOUS_STATUS -> ValidationResult.UNSAFE + else -> ValidationResult.FAILED_TO_VALIDATE + } + val simulationResponse = from.result.simulation + val simulation = if (simulationResponse == null) { + SimulationResult.FailedToSimulate + } else { + mapToSolanaAssetsDiffs(simulationResponse.accountSummary.accountAssetsDiff) + } + return CheckTransactionResult( + validation = validation, + description = from.result.validation.description, + simulation = simulation, + ) + } + fun mapToDomain(from: TransactionScanResponse): CheckTransactionResult { return CheckTransactionResult( validation = when { @@ -68,7 +91,7 @@ internal object BlockAidMapper { fun mapToSolanaRequest(from: TransactionData): SolanaTransactionScanRequest { return SolanaTransactionScanRequest( - chain = from.chain.lowercase(), + blockchain = from.chain.lowercase(), accountAddress = from.accountAddress, metadata = TransactionMetadata(from.domainUrl), method = from.method, @@ -78,35 +101,61 @@ internal object BlockAidMapper { private fun mapSimulationSuccessResult(from: AccountSummaryResponse): SimulationResult { return when { - !from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction( - from.assetsDiffs, - ) - !from.exposures.isNullOrEmpty() -> mapApproveTransaction( - from.exposures, - ) - !from.traces.isNullOrEmpty() -> mapNftSendReceiveTransaction(from.traces) + !from.exposures.isNullOrEmpty() -> mapApproveTransaction(from.exposures) + !from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction(from.assetsDiffs) else -> SimulationResult.Success(data = SimulationData.NoWalletChangesDetected) } } - private fun mapApproveTransaction(exposures: List?): SimulationResult { - val amounts = exposures?.flatMap { exposure -> - val tokenInfo = TokenInfo( - chainId = exposure.asset.chainId, - logoUrl = exposure.asset.logoUrl, - symbol = exposure.asset.symbol ?: "", - decimals = exposure.asset.decimals ?: 0, + private fun mapToSolanaAssetsDiffs(assetsDiffs: List): SimulationResult { + val sendInfo = assetsDiffs.mapNotNull { assetDiff -> + val outTransfer = assetDiff.outTransfer ?: return@mapNotNull null + val amount = outTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null + AmountInfo.FungibleTokens( + amount = amount, + token = TokenInfo( + chainId = null, + logoUrl = assetDiff.asset.logoUrl, + symbol = assetDiff.asset.assetSymbol(), + decimals = assetDiff.asset.decimals ?: 0, + ), ) - exposure.spenders.flatMap { (_, spender) -> - val isUnlimited = spender.isApprovedForAll == true - val approval = spender.approval?.hexToBigDecimal() - spender.exposure.map { detail -> - ApprovedAmount( - approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(), - isUnlimited = isUnlimited, - tokenInfo = tokenInfo, - ) - } + } + val receiveInfo = assetsDiffs.mapNotNull { assetDiff -> + val inTransfer = assetDiff.inTransfer ?: return@mapNotNull null + val amount = inTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null + AmountInfo.FungibleTokens( + amount = amount, + token = TokenInfo( + chainId = null, + logoUrl = assetDiff.asset.logoUrl, + symbol = assetDiff.asset.assetSymbol(), + decimals = assetDiff.asset.decimals ?: 0, + ), + ) + } + + return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) { + SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo)) + } else { + SimulationResult.Success(SimulationData.NoWalletChangesDetected) + } + } + + private fun SolanaTransactionAsset.assetSymbol(): String { + return if (type?.lowercase().equals(SOL_ASSET_SYMBOL, ignoreCase = true)) { + symbol ?: SOL_ASSET_SYMBOL + } else { + symbol.orEmpty() + } + } + + private fun mapApproveTransaction(exposures: List?): SimulationResult { + val amounts: List? = exposures?.flatMap { exposure -> + if (exposure.assetType.isNFT()) { + listOf(mapApproveNftTransaction(exposure)) + } else { + mapTransaction(exposure) } } return if (!amounts.isNullOrEmpty()) { @@ -116,6 +165,34 @@ internal object BlockAidMapper { } } + private fun mapTransaction(exposure: Exposure): List { + val tokenInfo = TokenInfo( + chainId = exposure.asset.chainId, + logoUrl = exposure.asset.logoUrl, + symbol = exposure.asset.symbol ?: "", + decimals = exposure.asset.decimals ?: 0, + ) + return exposure.spenders.flatMap { (_, spender) -> + val isUnlimited = spender.isApprovedForAll == true + val approval = spender.approval?.hexToBigDecimal() + spender.exposure.map { detail -> + ApproveInfo.Amount( + approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(), + isUnlimited = isUnlimited, + tokenInfo = tokenInfo, + ) + } + } + } + + private fun mapApproveNftTransaction(exposure: Exposure): ApproveInfo.NonFungibleToken { + return ApproveInfo.NonFungibleToken( + name = exposure.asset.name.orEmpty(), + logoUrl = exposure.spenders.values.firstOrNull()?.exposure?.firstOrNull()?.logoUrl + ?: exposure.asset.logoUrl, + ) + } + private fun mapSendReceiveTransaction(assetDiffs: List?): SimulationResult { val sendInfo = arrayListOf() val receiveInfo = arrayListOf() @@ -128,13 +205,31 @@ internal object BlockAidMapper { decimals = diff.asset.decimals ?: 0, ) diff.outTransfer.orEmpty().forEach { transfer -> - transfer.value?.toBigDecimalOrNull()?.let { amount -> - sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + if (diff.assetType.isNFT()) { + sendInfo.add( + AmountInfo.NonFungibleTokens( + name = diff.asset.name.orEmpty(), + logoUrl = token.logoUrl, + ), + ) + } else { + transfer.value?.toBigDecimalOrNull()?.let { amount -> + sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + } } } diff.inTransfer.orEmpty().forEach { transfer -> - transfer.value?.toBigDecimalOrNull()?.let { amount -> - receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + if (diff.assetType.isNFT()) { + receiveInfo.add( + AmountInfo.NonFungibleTokens( + name = diff.asset.name.orEmpty(), + logoUrl = token.logoUrl, + ), + ) + } else { + transfer.value?.toBigDecimalOrNull()?.let { amount -> + receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + } } } } @@ -146,17 +241,7 @@ internal object BlockAidMapper { } } - private fun mapNftSendReceiveTransaction(traces: List?): SimulationResult { - val sendInfo = traces?.mapNotNull { - it.exposed?.let { exposed -> - AmountInfo.NonFungibleTokens(name = "${it.asset.name} #${exposed.tokenId}", logoUrl = exposed.logoUrl) - } - } - - return if (!sendInfo.isNullOrEmpty()) { - SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = listOf())) - } else { - SimulationResult.Success(SimulationData.NoWalletChangesDetected) - } + private fun String.isNFT(): Boolean { + return this.lowercase() == "erc721" || this.lowercase() == "erc1155" || this.lowercase() == "nft" } } \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt index 66483ed2df..985e8d7e2e 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -24,16 +24,21 @@ internal class DefaultBlockAidRepository( } override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult { - val response = withContext(dispatchers.io) { - when (data.params) { - is TransactionParams.Evm -> { - api.scanJsonRpc(mapper.mapToEvmRequest(data)) - } - is TransactionParams.Solana -> { - api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) - } - } + return when (data.params) { + is TransactionParams.Evm -> scanEvmTransaction(data = data) + is TransactionParams.Solana -> scanSolanaTransaction(data = data) } - return mapper.mapToDomain(response) } + + private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult = + withContext(dispatchers.io) { + val response = api.scanJsonRpc(mapper.mapToEvmRequest(data)) + mapper.mapToDomain(response) + } + + private suspend fun scanSolanaTransaction(data: TransactionData): CheckTransactionResult = + withContext(dispatchers.io) { + val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) + mapper.mapToDomain(response) + } } \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt index 00dd54e198..3b24f6e0a4 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -4,6 +4,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.google.common.truth.Truth import com.tangem.datasource.api.common.blockaid.models.response.* @@ -44,6 +45,7 @@ class BlockAidMapperTest { val exposure = Exposure( asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE", decimals = 8), spenders = mapOf("spender" to spenderDetails), + assetType = "native", ) val response = TransactionScanResponse( validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), @@ -65,9 +67,10 @@ class BlockAidMapperTest { val approve = simulation?.data as? SimulationData.Approve Truth.assertThat(approve).isNotNull() - Truth.assertThat(approve?.approvedAmounts?.size).isEqualTo(1) - Truth.assertThat(approve?.approvedAmounts?.first()?.approvedAmount).isEqualTo(BigDecimal("1000.0")) - Truth.assertThat(approve?.approvedAmounts?.first()?.isUnlimited).isTrue() + Truth.assertThat(approve?.items?.size).isEqualTo(1) + Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.approvedAmount) + .isEqualTo(BigDecimal("1000.0")) + Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.isUnlimited).isTrue() } @Test diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt index d125246bce..54d8331f1f 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -11,6 +11,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 com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -91,7 +92,7 @@ class DefaultBlockAidRepositoryTest { ) val request = mockk() - val response = mockk() + val response = mockk() val expectedResult = mockk() every { mapper.mapToSolanaRequest(data) } returns request diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 18929af1a4..eca48639fe 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -9,6 +9,7 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher @@ -42,6 +43,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, + private val userTokenSaver: UserTokensSaver, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val userTokensResponseStore: UserTokensResponseStore, private val testnetTokensStorage: TestnetTokensStorage, @@ -127,7 +129,8 @@ internal class DefaultManageTokensRepository( val tokensResponse = request.params.userWalletId?.let { userWalletId -> if (loadUserTokensFromRemote && userWallet != null) { safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) { - createDefaultUserTokensResponse(userWallet) + // save tokens response only if loadUserTokensFromRemote is true and it means onboarding call + createAndSaveDefaultUserTokensResponse(userWallet = userWallet) } } else { getSavedUserTokensResponseSync(userWalletId) @@ -158,6 +161,12 @@ internal class DefaultManageTokensRepository( ) } + private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { + val userTokensResponse = createDefaultUserTokensResponse(userWallet) + userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false) + return userTokensResponse + } + private suspend fun fetchTestnetCurrencies( userWallet: UserWallet, request: Request, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 87943d1e88..412155dcda 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -32,6 +32,7 @@ internal object ManageTokensDataModule { userWalletsStore: UserWalletsStore, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, userTokensResponseStore: UserTokensResponseStore, + userTokensSaver: UserTokensSaver, testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, @@ -43,6 +44,7 @@ internal object ManageTokensDataModule { userWalletsStore = userWalletsStore, manageTokensUpdateFetcher = manageTokensUpdateFetcher, userTokensResponseStore = userTokensResponseStore, + userTokenSaver = userTokensSaver, testnetTokensStorage = testnetTokensStorage, excludedBlockchains = excludedBlockchains, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index a9d3b05d9b..7179221efb 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -381,6 +381,26 @@ internal class DefaultTransactionRepository( preparer.prepareForSendMultiple(transactionData, signer) } + override suspend fun prepareAndSign( + transactionData: TransactionData, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(dispatchers.io) { + val preparer = getPreparer(network, userWalletId) + preparer.prepareAndSign(transactionData, signer) + } + + override suspend fun prepareAndSignMultiple( + transactionData: List, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(dispatchers.io) { + val preparer = getPreparer(network, userWalletId) + preparer.prepareAndSignMultiple(transactionData, signer) + } + private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ff1d5fb9ef..38d1d2957a 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { /** Libs - Tangem */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(projects.libs.tangemSdkApi) /** DI */ implementation(deps.hilt.core) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt index e42238404f..cc1a69895e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt @@ -2,39 +2,52 @@ package com.tangem.data.pay import arrow.core.Either import com.squareup.moshi.Moshi +import com.tangem.common.map import com.tangem.core.error.UniversalError import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.KycStartInfo import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.sdk.api.TangemSdkManager import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.withContext -@Suppress("UnusedPrivateMember") class DefaultKycRepository @AssistedInject constructor( - @Assisted userWalletId: UserWalletId, @NetworkMoshi moshi: Moshi, private val tangemPayApi: TangemPayApi, - private val dispatcherProvider: CoroutineDispatcherProvider, + private val visaAuthRepository: VisaAuthRepository, + private val tangemSdkManager: TangemSdkManager, ) : KycRepository { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) - override suspend fun getKycStartInfo(): Either = withContext(dispatcherProvider.io) { - val authTokenForSpecificWallet = "get from userWalletId" - - request { - tangemPayApi.getKycAccess( - authHeader = authTokenForSpecificWallet, - ).getOrThrow().result + override suspend fun getKycStartInfo(address: String, cardId: String): Either { + var authHeader = "" + visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result -> + tangemSdkManager.visaCustomerWalletApprove( + VisaDataForApprove( + customerWalletCardId = cardId, + targetAddress = address, + dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge), + ), + ).map { signResult -> + visaAuthRepository.getTokenWithCustomerWallet( + sessionId = result.session.sessionId, + signature = signResult.signature, + nonce = signResult.dataToSign.hashToSign, + ).getOrNull()?.let { authHeader = it } + } + } + return request { + authHeader.ifEmpty { error("Cannot get auth header for KYC") } + tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result }.map { KycStartInfo( token = it.token, @@ -65,6 +78,6 @@ class DefaultKycRepository @AssistedInject constructor( @AssistedFactory interface Factory : KycRepository.Factory { - override fun create(userWalletId: UserWalletId): DefaultKycRepository + override fun create(): DefaultKycRepository } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 30457ea047..b38b9a82a8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -131,16 +131,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - visaApi.activateByCustomerWallet( - authHeader = authTokens.getAuthHeader(), - body = ActivationByCustomerWalletRequest( - orderId = signedData.dataToSign.request.orderId, - customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( - deployAcceptanceSignature = signedData.signature, - customerWalletAddress = signedData.customerWalletAddress, + signedData.dataToSign.request?.orderId?.let { orderId -> + visaApi.activateByCustomerWallet( + authHeader = authTokens.getAuthHeader(), + body = ActivationByCustomerWalletRequest( + orderId = orderId, + customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( + deployAcceptanceSignature = signedData.signature, + customerWalletAddress = signedData.customerWalletAddress, + ), ), - ), - ).getOrThrow() + ).getOrThrow() + } ?: error("Order Id cannot be null") } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index c9c07624e5..8b3024f1fe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -65,6 +65,39 @@ internal class DefaultVisaAuthRepository @Inject constructor( } } + override suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.generateNonceByCustomerWallet( + GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress), + ).getOrThrow() + }.map { response -> + VisaAuthChallenge.Wallet( + challenge = response.result.nonce, + session = VisaAuthSession(response.result.sessionId), + ) + } + } + + override suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.getTokenByCustomerWallet( + GetTokenByCustomerWalletRequest( + sessionId = sessionId, + signature = signature, + messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + ), + ).getOrThrow() + }.map { response -> + "Bearer ${response.result.accessToken}" + } + } + override suspend fun getAccessTokens( signedChallenge: VisaAuthSignedChallenge, ): Either = withContext(dispatchers.io) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index 0bc201fd7a..bf9f6674f9 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -1,9 +1,21 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.Wallet.Model +import com.reown.walletkit.client.WalletKit +import com.tangem.blockchain.extensions.hexToInt +import com.tangem.data.walletconnect.model.CAIP10 +import com.tangem.data.walletconnect.model.CAIP2 +import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork.NamespaceConverter.Companion.ETH_NAMESPACE_KEY import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.walletconnect.model.HandleMethodError import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession @@ -13,9 +25,13 @@ import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume internal class WcEthAddNetworkUseCase @AssistedInject constructor( private val respondService: WcRespondService, + private val networksConverter: WcNetworksConverter, + addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory, @Assisted val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.AddEthereumChain, ) : WcAddNetworkUseCase { @@ -31,10 +47,63 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( else -> WcNetworkDerivationState.Single } + private val addSwitchCommonDelegate = addSwitchCommonDelegateFactory.create(context) + + override suspend fun invoke(): Either { + return addSwitchCommonDelegate + .commonChecks(method.rawChain.chainId) + .map { addedNetwork -> + WcAddNetworkUseCase.AddNetwork( + network = addedNetwork, + isExistInWcSession = addSwitchCommonDelegate.existInWcSession(addedNetwork), + ) + } + } + override suspend fun approve(): Either { + fun illegalState() = WcRequestError.UnknownError(IllegalStateException("IllegalStateException")).left() + val requestedNetworkCAIP2 = CAIP2.fromRaw(rawSdkRequest.chainId.orEmpty()) ?: return illegalState() + val networkToAddCAIP2 = addSwitchCommonDelegate.hexChainIdToCAIP2(method.rawChain.chainId) + ?: return illegalState() + val namespaces = session.sdkModel.namespaces[requestedNetworkCAIP2.namespace] + ?: return illegalState() + // find and add all derivation + val networkToAddCAIP10 = networksConverter + .allAddressForChain(networkToAddCAIP2.raw, wallet) + .map { address -> CAIP10(networkToAddCAIP2, address).raw } + val newNamespaces = namespaces.copy( + chains = namespaces.chains.plus(networkToAddCAIP2.raw), + accounts = namespaces.accounts.plus(networkToAddCAIP10), + ) + val sdkNewNamespaces = session.sdkModel.namespaces + .plus(requestedNetworkCAIP2.namespace to newNamespaces) + .mapValues { (_, session) -> + Model.Namespace.Session( + chains = session.chains, + accounts = session.accounts, + methods = session.methods, + events = session.events, + ) + } + + val sessionUpdate = Wallet.Params.SessionUpdate( + sessionTopic = context.session.sdkModel.topic, + namespaces = sdkNewNamespaces, + ) + sdkUpdateSession(sessionUpdate) // ignore result for now return respondService.respond(rawSdkRequest, "") } + private suspend fun sdkUpdateSession(sessionUpdate: Wallet.Params.SessionUpdate): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.updateSession( + params = sessionUpdate, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) }, + ) + } + } + override fun reject() { respondService.rejectRequestNonBlock(rawSdkRequest) } @@ -43,4 +112,37 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( interface Factory { fun create(context: WcMethodUseCaseContext, method: WcEthMethod.AddEthereumChain): WcEthAddNetworkUseCase } +} + +internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor( + private val networksConverter: WcNetworksConverter, + @Assisted val context: WcMethodUseCaseContext, +) { + + private val wallet: UserWallet get() = context.session.wallet + + fun hexChainIdToCAIP2(hexChainId: String): CAIP2? = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${hexChainId.hexToInt()}") + + fun existInWcSession(network: Network): Boolean { + return context.session.networks.any { it.rawId == network.rawId } + } + + suspend fun commonChecks(hexChainId: String): Either { + val caip2 = hexChainIdToCAIP2(hexChainId) + ?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left() + val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet) + if (generalNetwork == null) { + return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() + } + val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(caip2.raw, wallet) + if (addedNetwork == null) { + return HandleMethodError.NotAddedNetwork(generalNetwork.name).left() + } + return addedNetwork.right() + } + + @AssistedFactory + interface Factory { + fun create(context: WcMethodUseCaseContext): WcEthAddSwitchCommonDelegate + } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 0f9e21543f..a14460a566 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -14,7 +14,6 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Compani import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.data.walletconnect.utils.WcNetworksConverter -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -44,7 +43,7 @@ internal class WcEthNetwork( ?: return HandleMethodError.UnknownSession.left() val wallet = session.wallet val chainId = request.chainId.orEmpty() - val method: WcEthMethod = name.toMethod(request, wallet) + val method: WcEthMethod = name.toMethod(request) .getOrElse { return error(it.message.orEmpty()) } ?: return error("Failed to parse $name") suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) @@ -54,7 +53,9 @@ internal class WcEthNetwork( is WcEthMethod.SendTransaction -> method.transaction.from is WcEthMethod.SignTransaction -> method.transaction.from is WcEthMethod.SignTypedData -> method.account - is WcEthMethod.AddEthereumChain -> + is WcEthMethod.AddEthereumChain, + is WcEthMethod.SwitchEthereumChain, + -> anyExistNetwork() ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } .orEmpty() @@ -65,7 +66,9 @@ internal class WcEthNetwork( is WcEthMethod.SendTransaction, is WcEthMethod.SignTransaction, -> networksConverter.findWalletNetworkForRequest(request, session, accountAddress) - is WcEthMethod.AddEthereumChain -> anyExistNetwork() + is WcEthMethod.AddEthereumChain, + is WcEthMethod.SwitchEthereumChain, + -> anyExistNetwork() } ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") val context = WcMethodUseCaseContext( @@ -81,13 +84,11 @@ internal class WcEthNetwork( is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method) is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method) is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method) + is WcEthMethod.SwitchEthereumChain -> factories.switchNetwork.create(context, method) }.right() } - private suspend fun WcEthMethodName.toMethod( - request: WcSdkSessionRequest, - wallet: UserWallet, - ): Either { + private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest): Either { val rawParams = request.request.params return when (this) { WcEthMethodName.EthSign, @@ -109,14 +110,17 @@ internal class WcEthNetwork( } } ?: return null.right() - WcEthMethodName.AddEthereumChain -> moshi.fromJson>(rawParams) + WcEthMethodName.AddEthereumChain, + WcEthMethodName.SwitchEthereumChain, + -> moshi.fromJson>(rawParams) .getOrElse { return it.left() } ?.firstOrNull() ?.let { - val newNetwork = networksConverter - .mainOrAnyWalletNetworkForRequest(it.chainId, wallet) - ?: return null.right() - WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork).right() + if (this == WcEthMethodName.AddEthereumChain) { + WcEthMethod.AddEthereumChain(rawChain = it).right() + } else { + WcEthMethod.SwitchEthereumChain(rawChain = it).right() + } } ?: null.right() } @@ -147,13 +151,17 @@ internal class WcEthNetwork( override val excludedBlockchains: ExcludedBlockchains, ) : WcNamespaceConverter { - override val namespaceKey: NamespaceKey = NamespaceKey("eip155") + override val namespaceKey: NamespaceKey = NamespaceKey(ETH_NAMESPACE_KEY) override fun toBlockchain(chainId: CAIP2): Blockchain? { if (chainId.namespace != namespaceKey.key) return null val ethChainId = chainId.reference.toIntOrNull() ?: return null return Blockchain.fromChainId(ethChainId) } + + companion object { + const val ETH_NAMESPACE_KEY = "eip155" + } } internal class Factories @Inject constructor( @@ -162,5 +170,6 @@ internal class WcEthNetwork( val sendTransaction: WcEthSendTransactionUseCase.Factory, val signTransaction: WcEthSignTransactionUseCase.Factory, val addNetwork: WcEthAddNetworkUseCase.Factory, + val switchNetwork: WcEthSwitchNetworkUseCase.Factory, ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 5358c3a317..e82c497072 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -7,6 +7,9 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam.TxSentFrom +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -86,6 +89,16 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emit(state.toResult(parseSendError(error).left())) } .getOrNull() ?: return + analytics.send( + Basic.TransactionSent( + sentFrom = TxSentFrom.WalletConnect( + blockchain = network.name, + token = network.currencySymbol, + feeType = null, + ), + memoType = MemoType.Null, + ), + ) val respondResult = respondService.respond(rawSdkRequest, hash.formatHex()) emit(state.toResult(respondResult)) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index b6795bf29b..8cfe678df0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -30,7 +30,7 @@ import com.tangem.blockchain.common.Amount as BlockchainAmount internal class WcEthSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, - private val prepareForSend: PrepareForSendUseCase, + private val prepareForSend: PrepareForSendUseCase, // TODO: TODO("[REDACTED_JIRA]") private val ethTxHelper: WcEthTxHelper, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTransaction, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt new file mode 100644 index 0000000000..5b94908fa8 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt @@ -0,0 +1,56 @@ +package com.tangem.data.walletconnect.network.ethereum + +import arrow.core.Either +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.HandleMethodError +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState +import com.tangem.domain.walletconnect.usecase.method.WcSwitchNetworkUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class WcEthSwitchNetworkUseCase @AssistedInject constructor( + private val respondService: WcRespondService, + @Assisted val context: WcMethodUseCaseContext, + @Assisted override val method: WcEthMethod.SwitchEthereumChain, + addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory, +) : WcSwitchNetworkUseCase { + + override val session: WcSession + get() = context.session + override val rawSdkRequest: WcSdkSessionRequest + get() = context.rawSdkRequest + override val network: Network + get() = context.network + override val derivationState: WcNetworkDerivationState = when { + context.networkDerivationsCount > 1 -> WcNetworkDerivationState.Multiple(walletAddress = context.accountAddress) + else -> WcNetworkDerivationState.Single + } + + private val addSwitchCommonDelegate = addSwitchCommonDelegateFactory.create(context) + + override suspend fun invoke(): Either { + return addSwitchCommonDelegate + .commonChecks(method.rawChain.chainId) + .map { addedNetwork -> + WcSwitchNetworkUseCase.SwitchNetwork( + network = addedNetwork, + isExistInWcSession = addSwitchCommonDelegate.existInWcSession(addedNetwork), + ) + } + } + + override fun reject() { + respondService.rejectRequestNonBlock(rawSdkRequest) + } + + @AssistedFactory + interface Factory { + fun create(context: WcMethodUseCaseContext, method: WcEthMethod.SwitchEthereumChain): WcEthSwitchNetworkUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index 057afaa2e9..3526fb2d8c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -2,7 +2,7 @@ package com.tangem.data.walletconnect.network.ethereum import com.domain.blockaid.models.transaction.CheckTransactionResult import com.domain.blockaid.models.transaction.SimulationResult -import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData @@ -68,13 +68,15 @@ internal class WcEthTxHelper @Inject constructor( ) } - fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApprovedAmount? { + fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApproveInfo.Amount? { val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId val isApprovalWcMethod = txData?.startsWith(approvalMethodId) if (isApprovalWcMethod != true) return null val simulation = result.simulation as? SimulationResult.Success ?: return null - val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts + val approves = (simulation.data as? SimulationData.Approve) + ?.items + ?.filterIsInstance() ?: return null if (approves.isEmpty()) return null val amount = approves.first() diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt index 8e91c2c8dd..d8b9e39468 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate -import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck @@ -29,7 +29,7 @@ import org.json.JSONObject internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, - private val prepareForSend: PrepareForSendUseCase, + private val prepareAndSign: PrepareAndSignUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcSolanaMethod.SignAllTransaction, blockAidDelegate: BlockAidVerificationDelegate, @@ -46,7 +46,7 @@ internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor( ).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } } override suspend fun SignCollector>.onSign(state: WcSignState>) { - val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) + val hash = prepareAndSign.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> emit(state.toResult(parseSendError(error).left())) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt index 282125a721..c1d301135a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate -import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck @@ -27,7 +27,7 @@ import okio.ByteString.Companion.decodeBase64 internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, - private val prepareForSend: PrepareForSendUseCase, + private val prepareAndSign: PrepareAndSignUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcSolanaMethod.SignTransaction, blockAidDelegate: BlockAidVerificationDelegate, @@ -44,7 +44,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( ).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } } override suspend fun SignCollector.onSign(state: WcSignState) { - val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) + val hash = prepareAndSign.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> emit(state.toResult(parseSendError(error).left())) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index bfd1192118..ad67584bed 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -20,10 +20,12 @@ import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime import timber.log.Timber +import java.net.URI @Suppress("LongParameterList") internal class DefaultWcPairUseCase @AssistedInject constructor( @@ -63,6 +65,12 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( return@flow } + val dAppUri = URI(sdkSessionProposal.url) + if (dAppUri.host.isNullOrEmpty()) { + emit(WcPairState.Error(WcPairError.InvalidDomainURL)) + return@flow + } + val proposalState = buildProposalState(sdkSessionProposal, sdkVerifyContext) .onLeft { analytics.send(WcAnalyticEvents.PairFailed(it.code)) @@ -112,14 +120,21 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") } emit(WcPairState.Approving.Result(sessionForApprove, either)) - }.onCompletion { - if (it != null) { - Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") - emit(WcPairState.Error(WcPairError.Unknown(it.message.orEmpty()))) - } else { - Timber.tag(WC_TAG).i("Completed successfully $pairRequest") - } } + .catch { + val pairError: WcPairError = when (it) { + is TimeoutCancellationException -> WcPairError.TimeoutException(it.message.orEmpty()) + else -> WcPairError.Unknown(it.message.orEmpty()) + } + emit(WcPairState.Error(pairError)) + } + .onCompletion { + if (it != null) { + Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") + } else { + Timber.tag(WC_TAG).i("Completed successfully $pairRequest") + } + } } override fun approve(sessionForApprove: WcSessionApprove) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index e9665c0673..35902fb41f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -150,7 +150,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left() companion object { - private const val CALLBACK_TIMEOUT = 60 + private const val CALLBACK_TIMEOUT = 15 // com.reown.android.pairing.engine.domain.PairingEngine.pair private val pairingExpiredMessages = listOf( "Pairing URI expired", diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index 4c50b189fd..db8788c05a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -33,6 +33,7 @@ internal class DefaultWcRequestService( Timber.tag(WC_TAG).i("handle request name $name") if (name is WcMethodName.Unsupported) { respondService.rejectRequestNonBlock(sr) + if (name.raw.startsWith("wallet_")) return } _wcRequest.trySend(name to sr) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt index 3421317947..c8491f5be6 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt @@ -4,31 +4,32 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.models.network.Network import com.tangem.utils.converter.Converter -import javax.inject.Inject -internal class BlockAidChainNameConverter @Inject constructor() : Converter { +internal object BlockAidChainNameConverter : Converter { @Suppress("CyclomaticComplexMethod") - override fun convert(value: Network): String { + override fun convert(value: Network): String? { return when (Blockchain.fromNetworkId(value.backendId)) { Blockchain.Arbitrum -> "arbitrum" Blockchain.Avalanche -> "avalanche" Blockchain.AvalancheTestnet -> "avalanche-fuji" - Blockchain.Binance, Blockchain.BSC -> "bsc" - Blockchain.Ethereum -> "ethereum" - Blockchain.EthereumTestnet -> "ethereum-sepolia" - Blockchain.Polygon -> "polygon" - Blockchain.Solana -> "mainnet" - Blockchain.Gnosis -> "gnosis" - Blockchain.Optimism -> "optimism" - Blockchain.ZkSyncEra -> "zksync" - Blockchain.ZkSyncEraTestnet -> "zksync-sepolia" Blockchain.Base -> "base" Blockchain.BaseTestnet -> "base-sepolia" + Blockchain.Binance, Blockchain.BSC -> "bsc" + Blockchain.Ethereum -> "ethereum" + Blockchain.Optimism -> "optimism" + Blockchain.Polygon -> "polygon" + Blockchain.ZkSyncEra -> "zksync" + Blockchain.ZkSyncEraTestnet -> "zksync-sepolia" Blockchain.Blast, Blockchain.BlastTestnet -> "blast" - Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain" Blockchain.Scroll -> "scroll" - else -> value.name + Blockchain.EthereumTestnet -> "ethereum-sepolia" + Blockchain.Gnosis -> "gnosis" + Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain" + + Blockchain.Solana -> "mainnet" + + else -> null } } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index 4a774ac505..8bc9a0c492 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -17,7 +17,6 @@ import javax.inject.Inject internal class BlockAidVerificationDelegate @Inject constructor( private val blockAidVerifier: BlockAidVerifier, - private val blockAidChainNameConverter: BlockAidChainNameConverter, ) { fun getSecurityStatus( @@ -27,7 +26,6 @@ internal class BlockAidVerificationDelegate @Inject constructor( session: WcSession, accountAddress: String?, ): LceFlow = flow { - emit(Lce.Loading(partialContent = null)) val failedResult = CheckTransactionResult( validation = ValidationResult.FAILED_TO_VALIDATE, simulation = SimulationResult.FailedToSimulate, @@ -36,7 +34,21 @@ internal class BlockAidVerificationDelegate @Inject constructor( emit(Lce.Content(failedResult)) return@flow } - when (method) { + val chain = BlockAidChainNameConverter.convert(network) + if (chain == null) { + emit(Lce.Content(failedResult)) + return@flow + } + emit(Lce.Loading(partialContent = null)) + val methodName = when (method) { + is WcEthMethod -> rawSdkRequest.request.method + is WcSolanaMethod -> method.trimmedPrefixMethodName + is WcMethod.Unsupported -> { + emit(Lce.Content(failedResult)) + return@flow + } + } + val params = when (method) { is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params) is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction) is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction)) @@ -45,25 +57,28 @@ internal class BlockAidVerificationDelegate @Inject constructor( emit(Lce.Content(failedResult)) return@flow } - else -> null - }?.let { params -> - blockAidVerifier.verifyTransaction( - TransactionData( - chain = blockAidChainNameConverter.convert(network), - accountAddress = accountAddress, - method = rawSdkRequest.request.method, - domainUrl = session.sdkModel.appMetaData.url, - params = params, - ), - ).fold( - ifLeft = { - Timber.e("Failed to verify transaction: ${it.localizedMessage}") - emit(Lce.Error(it)) - }, - ifRight = { - emit(Lce.Content(it)) - }, - ) - } ?: emit(Lce.Content(failedResult)) + else -> { + emit(Lce.Content(failedResult)) + return@flow + } + } + + blockAidVerifier.verifyTransaction( + data = TransactionData( + chain = chain, + accountAddress = accountAddress, + method = methodName, + domainUrl = session.sdkModel.appMetaData.url, + params = params, + ), + ).fold( + ifLeft = { + Timber.e("Failed to verify transaction: ${it.localizedMessage}") + emit(Lce.Error(it)) + }, + ifRight = { + emit(Lce.Content(it)) + }, + ) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 6a80763cc9..a7d1d285fb 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -25,6 +25,11 @@ internal class WcNetworksConverter @Inject constructor( private val tokensFeatureToggles: TokensFeatureToggles, ) { + fun createNetwork(chainId: String, wallet: UserWallet): Network? { + return namespaceConverters + .firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } + } + suspend fun findWalletNetworkForRequest( request: WcSdkSessionRequest, session: WcSession, @@ -48,6 +53,11 @@ internal class WcNetworksConverter @Inject constructor( return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull() } + suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List { + return filterWalletNetworkForRequest(rawChainId, wallet) + .mapNotNull { walletManagersFacade.getDefaultAddress(wallet.walletId, it)?.lowercase() } + } + /** * return all exist derivation networks */ diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt index 0f2e570acd..f588c4f171 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt @@ -10,6 +10,14 @@ internal object WcSdkSessionConverter : Converter + WcSdkSession.Session( + chains = session.chains ?: listOf(), + accounts = session.accounts, + methods = session.methods, + events = session.events, + ) + }, ) } } \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index df3b560b15..573d2949bc 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -47,7 +47,7 @@ internal class DefaultWcPairUseCaseTest { pairingTopic = "", name = "", description = "", - url = "", + url = "https://react-app.walletconnect.com/", icons = listOf(), redirect = "", requiredNamespaces = mapOf(), diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt index e68555c7c1..15049820a7 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -62,6 +62,7 @@ internal class WcSignUseCaseDelegateTest { connectingTime = 0L, sdkModel = WcSdkSession( topic = "", + namespaces = mapOf(), appMetaData = WcAppMetaData( name = "", description = "", diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index eff1e685f6..edd6da8af5 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -10,6 +10,7 @@ import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber @@ -41,7 +42,7 @@ internal class WalletManagerFactory( blockchain: Blockchain, derivationPath: DerivationPath?, ): WalletManager? { - val curve = blockchain.getSupportedCurves().first() + val curve = hotWallet.curvesConfig.primaryCurve(blockchain) val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } ?: return null return try { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 8808bd336a..2127d9b80b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -17,6 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFI import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.wallet.UserWallet @@ -47,14 +48,78 @@ internal class DefaultWalletsRepository( return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override fun shouldSaveUserWallets(): Flow { return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override suspend fun saveShouldSaveUserWallets(item: Boolean) { appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item) } + override suspend fun useBiometricAuthentication(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + ) + + if (useBiometricAuthentication != null) { + return useBiometricAuthentication + } + + val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SAVE_USER_WALLETS_KEY, + ) + + if (legacySaveWalletsInTheApp != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + value = legacySaveWalletsInTheApp, + ) + return legacySaveWalletsInTheApp + } else { + // Default value for new users + setUseBiometricAuthentication(false) + return false + } + } + + override suspend fun setUseBiometricAuthentication(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, value = value) + } + + override suspend fun requireAccessCode(): Boolean { + val requireAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + ) + + if (requireAccessCode != null) { + return requireAccessCode + } + + val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, + ) + + if (legacyShouldSaveAccessCode != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + value = legacyShouldSaveAccessCode.not(), + ) + return legacyShouldSaveAccessCode.not() + } else { + // Default value for new users + setRequireAccessCode(true) + return true + } + } + + override suspend fun setRequireAccessCode(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value) + } + override suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean { return appPreferencesStore .getSyncOrDefault(key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index dffa80fd65..ce12ab7516 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -7,12 +7,11 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.operations.derivation.ExtendedPublicKeysMap import kotlin.collections.forEach @@ -51,13 +50,9 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } private fun List.mapToNewDerivations(): List { - val config = when (userWallet) { - is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card) - is UserWallet.Hot -> Wallet2CardConfig // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet - } return mapNotNull { network -> val blockchain = network.toBlockchain() - val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null + val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null val walletPublicKey = when (userWallet) { is UserWallet.Cold -> { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 5564622241..bcaff10b6f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.data.wallets.derivations.DefaultDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository +import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore @@ -13,6 +14,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -68,4 +70,10 @@ internal interface WalletsDataBindsModule { @Binds @Singleton fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository + + @Binds + @Singleton + fun bindHotWalletAccessCodeAttemptsRepository( + impl: DefaultHotWalletAccessCodeAttemptsRepository, + ): HotWalletAccessCodeAttemptsRepository } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..8a77f2f70a --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,138 @@ +package com.tangem.data.wallets.hot + +import android.content.Context +import android.os.SystemClock +import android.provider.Settings +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.COOLDOWN_SECONDS +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS +import com.tangem.hot.sdk.model.HotWalletId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@Suppress("MagicNumber") +class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val appPreferencesStore: AppPreferencesStore, +) : HotWalletAccessCodeAttemptsRepository { + + override suspend fun incrementAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + val attemptsKey = PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey()) + + appPreferencesStore.editData { preferences -> + val currentAttempts = preferences[attemptsKey] ?: 0 + val newAttempts = currentAttempts + 1 + + preferences[attemptsKey] = newAttempts + val currentBootCount = currentBootCount() + preferences[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] = currentBootCount + + if (newAttempts >= MAX_FAST_FORWARD_ATTEMPTS) { + val currentDeadline = SystemClock.elapsedRealtime() + COOLDOWN_SECONDS * 1000 + preferences[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] = currentDeadline + } + } + } + + override suspend fun resetAttempts(hotWalletId: HotWalletId) { + val authAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = true, + ) + val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = false, + ) + + appPreferencesStore.editData { + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Flow { + val flow = appPreferencesStore.data.map { + AttemptsPersistentData( + attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0, + bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0, + deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L, + ) + }.distinctUntilChanged() + + return flow.transformLatest { + while (true) { + emit(toState(id, it.attempts, it.deadline, it.bootCount)) + val remaining = remainingSeconds(it.deadline, it.bootCount) + if (remaining <= 0) break + delay(timeMillis = 1000) + } + }.distinctUntilChanged() + } + + override suspend fun getAttemptsSync(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Attempts { + val prefs = appPreferencesStore.data.first() + val count = prefs[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0 + val boot = prefs[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0 + val deadline = prefs[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L + return toState(id, count, deadline, boot) + } + + private fun remainingSeconds(deadline: Long, bootStored: Int): Int { + val now = SystemClock.elapsedRealtime() + val bootNow = currentBootCount() + if (bootNow != bootStored) { + // If the boot happened after the last attempt, we consider timer to start from the beginning + return maxOf(0, COOLDOWN_SECONDS - (now / 1000).toInt()) + } + return maxOf(0, ((deadline - now) / 1000).toInt()) + } + + private fun toState( + id: HotWalletAccessCodeAttemptsRepository.AttemptId, + count: Int, + deadlineElapsed: Long, + bootStored: Int, + ): Attempts { + val fast = MAX_FAST_FORWARD_ATTEMPTS + val attention = ATTEMPTS_BEFORE_DELETION + val deletion = MAX_ATTEMPTS_BEFORE_DELETION + + return when { + count < fast -> Attempts.FastForward(count) + id.auth && count >= deletion -> Attempts.Deletion + id.auth && count >= attention -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.BeforeDeletion(count, remaining, deletion - count) + } + else -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.WithDelay(count, remaining) + } + } + } + + private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { + return "${hotWalletId.value}_$auth" + } + + private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) + + private data class AttemptsPersistentData( + val attempts: Int, + val bootCount: Int, + val deadline: Long, + ) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index c7569b5a3b..b5ca40b2de 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -1,7 +1,11 @@ package com.tangem.data.wallets.hot import com.tangem.common.core.TangemSdkError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.copy import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* @@ -9,7 +13,9 @@ import javax.inject.Inject class HotWalletAccessor @Inject constructor( private val tangemHotSdk: TangemHotSdk, + private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletPasswordRequester: HotWalletPasswordRequester, + private val walletsRepository: WalletsRepository, ) { suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = @@ -23,10 +29,24 @@ class HotWalletAccessor @Inject constructor( } private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth - HotWalletId.AuthType.Password -> requestPassword(false) - HotWalletId.AuthType.Biometry -> HotAuth.Biometry + HotWalletId.AuthType.Password -> requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) + HotWalletId.AuthType.Biometry -> { + if (isAccessCodeRequired) { + requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) + } else { + HotAuth.Biometry + } + } } return runCatchingSdkErrors(hotWalletId, auth) { @@ -42,27 +62,49 @@ class HotWalletAccessor @Inject constructor( block: suspend (auth: HotAuth) -> T, ): T { return runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = auth, auth = auth, block = { blockAuth -> block(blockAuth).also { - // TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code - // if user has biometry enabled, we set it as the new auth method - if (blockAuth is HotAuth.Password /*&& has biometry enabled */) { - tangemHotSdk.changeAuth( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = blockAuth, - ), - auth = HotAuth.Biometry, - ) - } + // Update biometry auth if the original auth was password + updateBiometryAuthIfNeeded( + hotWalletId = hotWalletId, + originalAuth = blockAuth, + ) } }, ) } + private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + + if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) { + val userWallet = userWalletsListRepository.userWalletsSync() + .find { it is UserWallet.Hot && it.hotWalletId == hotWalletId } + as? UserWallet.Hot + ?: return + + val newHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = originalAuth, + ), + auth = HotAuth.Biometry, + ) + + userWalletsListRepository.saveWithoutLock( + userWallet = userWallet.copy( + hotWalletId = newHotWalletId, + ), + canOverride = true, + ) + } + } + private suspend fun runCatchingWrongPassInternal( + hotWalletId: HotWalletId, originalAuth: HotAuth, auth: HotAuth, block: suspend (auth: HotAuth) -> T, @@ -71,9 +113,13 @@ class HotWalletAccessor @Inject constructor( }.getOrElse { exception -> if (auth is HotAuth.Biometry && exception.isBiometryError()) { // fallback to password if biometry fails - val passAuth = requestPassword(true) + val passAuth = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = true, + ) return@getOrElse runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = originalAuth, auth = passAuth, block = block, @@ -87,17 +133,28 @@ class HotWalletAccessor @Inject constructor( // If the exception is a wrong password, we need to request the password again hotWalletPasswordRequester.wrongPassword() - val passResult = requestPassword(originalAuth is HotAuth.Biometry) + val passResult = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = originalAuth is HotAuth.Biometry, + ) runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = originalAuth, auth = passResult, block = block, ) } - private suspend fun requestPassword(hasBiometry: Boolean): HotAuth { - return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled() + private suspend fun requestPassword(hotWalletId: HotWalletId, hasBiometry: Boolean): HotAuth { + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = false, + hasBiometry = hasBiometry, + ) + + return hotWalletPasswordRequester.requestPassword(attemptRequest).toAuth() + ?: throw TangemSdkError.UserCancelled() } private fun Throwable.isBiometryError(): Boolean { diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 2911dd5968..447c2effdf 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -195,7 +195,7 @@ class DefaultWalletsRepositoryTest { ) val authProvider = mockk { - every { getCardsPublicKeys() } returns publicKeys + coEvery { getCardsPublicKeys() } returns publicKeys } repository = DefaultWalletsRepository( diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index cf1bc96831..75db105c86 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -10,10 +10,12 @@ tasks.withType().configureEach { dependencies { + api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) implementation(deps.kotlin.serialization) testImplementation(deps.test.coroutine) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 319babf91b..89f7fd33c8 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -3,7 +3,10 @@ package com.tangem.domain.account.models import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable @@ -22,6 +25,8 @@ data class AccountList private constructor( val userWallet: UserWallet, val accounts: Set, val totalAccounts: Int, + val sortType: TokensSortType, + val groupType: TokensGroupType, ) { /** Retrieves the main crypto portfolio account from the list of accounts */ @@ -48,6 +53,8 @@ data class AccountList private constructor( userWallet = this.userWallet, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + sortType = this.sortType, + groupType = this.groupType, ) } @@ -68,6 +75,8 @@ data class AccountList private constructor( userWallet = this.userWallet, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, + sortType = this.sortType, + groupType = this.groupType, ) } @@ -132,6 +141,8 @@ data class AccountList private constructor( userWallet: UserWallet, accounts: Set, totalAccounts: Int, + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, ): Either = either { ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } @@ -149,10 +160,16 @@ data class AccountList private constructor( val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } - val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size + val uniqueAccountNameCount = accounts.map { it.accountName.value }.distinct().size ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames } - AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) + AccountList( + userWallet = userWallet, + accounts = accounts, + totalAccounts = totalAccounts, + sortType = sortType, + groupType = groupType, + ) } /** @@ -160,13 +177,23 @@ data class AccountList private constructor( * * @param userWallet the user wallet associated with the account list */ - fun empty(userWallet: UserWallet): AccountList { + fun empty( + userWallet: UserWallet, + cryptoCurrencies: Set = emptySet(), + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, + ): AccountList { return AccountList( userWallet = userWallet, accounts = setOf( - Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId), + Account.CryptoPortfolio.createMainAccount( + userWalletId = userWallet.walletId, + cryptoCurrencies = cryptoCurrencies, + ), ), totalAccounts = 1, + sortType = sortType, + groupType = groupType, ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index a32796f0a1..ac6921e167 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Repository interface for performing CRUD operations on accounts @@ -38,20 +39,47 @@ interface AccountsCRUDRepository { */ suspend fun getArchivedAccount(accountId: AccountId): Option + /** + * Retrieves a list of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not + */ + suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> + + /** + * Provides a flow of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + fun getArchivedAccounts(userWalletId: UserWalletId): Flow> + + /** + * Fetches archived accounts for a specific user wallet and updates the repository + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) + /** * Saves a list of accounts to the repository * * @param accountList the list of accounts to be saved. */ - @Throws suspend fun saveAccounts(accountList: AccountList) + /** + * Retrieves the total count of accounts associated with a specific user wallet including archived accounts + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int + /** * Retrieves a user wallet by its unique identifier * * @param userWalletId the unique identifier of the user wallet * @return the [UserWallet] associated with the given identifier */ - @Throws fun getUserWallet(userWalletId: UserWalletId): UserWallet } \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 75722f9824..6a3866bc4b 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -64,14 +64,9 @@ class AddCryptoPortfolioUseCase( return Account.CryptoPortfolio( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = accountName, - accountIcon = icon, + icon = icon, derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) } @@ -88,7 +83,13 @@ class AddCryptoPortfolioUseCase( catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - return AccountList.empty(userWallet = userWallet) + // TODO: [REDACTED_JIRA] + return AccountList.empty( + userWallet = userWallet, + cryptoCurrencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) } private suspend fun Raise.saveAccounts(accountList: AccountList) { diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt new file mode 100644 index 0000000000..cbcfb13168 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.launch + +typealias ArchivedAccountList = List + +/** + * Use case for retrieving archived accounts for a specific user wallet + * + * @property crudRepository the repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetArchivedAccountsUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Executes the use case to retrieve archived accounts for the given user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + operator fun invoke(userWalletId: UserWalletId): LceFlow = channelFlow { + val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId) + + archivedAccounts + .onRight { send(it.lceContent()) } + .onLeft { + send(lceLoading()) + + launch { + fetchArchivedAccounts(userWalletId).getOrElse { + send(it.lceError()) + } + } + } + + subscribeOnArchivedAccounts(userWalletId) + } + .distinctUntilChanged() + + private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { + crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse { + error("Archived accounts not found for user wallet: $userWalletId") + } + } + } + + private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) } + } + + private suspend fun ProducerScope>.subscribeOnArchivedAccounts( + userWalletId: UserWalletId, + ) { + crudRepository.getArchivedAccounts(userWalletId) + .distinctUntilChanged() + .retryWhen { cause, _ -> + send(cause.lceError()) + + delay(timeMillis = 2000) + + true + } + .collectLatest { archivedAccounts -> + send(archivedAccounts.lceContent()) + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt new file mode 100644 index 0000000000..c34240e22b --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for retrieving the next unoccupied account index + * + * @property crudRepository repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetUnoccupiedAccountIndexUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Invokes the use case to calculate the next unoccupied account index + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val totalAccountsCount = getTotalAccountsCount(userWalletId = userWalletId) + + DerivationIndex(totalAccountsCount + 1).getOrElse { + raise(Error.InvalidDerivationIndex(it)) + } + } + + private suspend fun Raise.getTotalAccountsCount(userWalletId: UserWalletId): Int { + return catch( + block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur in the use case + */ + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error" + + /** Error indicating that the derivation index is invalid */ + data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error { + override fun toString(): String = "$tag: Invalid derivation index: $cause" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt index 9679a036b6..f5dcec41aa 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -8,8 +8,6 @@ import arrow.core.raise.either import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId @@ -66,14 +64,10 @@ class RecoverCryptoPortfolioUseCase( return Account.CryptoPortfolio( accountId = this.accountId, accountName = this.name, - accountIcon = this.icon, + icon = this.icon, derivationIndex = this.derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + // TODO: [REDACTED_JIRA] + cryptoCurrencies = emptySet(), ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index 8c2208e552..4451a5f50d 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -79,7 +79,7 @@ class UpdateCryptoPortfolioUseCase( } private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio { - return if (icon != null) this.copy(accountIcon = icon) else this + return if (icon != null) this.copy(icon = icon) else this } /** diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index d5323c0a85..0c43198fa0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -123,7 +123,7 @@ class AccountListTest { accounts = setOf( Account.CryptoPortfolio.createMainAccount(userWalletId), Account.CryptoPortfolio.createMainAccount(userWalletId).copy( - accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), ), ), expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index cf47f9b807..f30bc1dfda 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -46,7 +46,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -75,7 +75,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -107,7 +107,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -138,7 +138,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -170,7 +170,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index aaea0a5379..1e442938fa 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -39,8 +39,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! val accountId = account.accountId - val archivedAccount = account.copy(isArchived = true) - val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + val updatedAccountList = (accountList - account).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() @@ -130,8 +129,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! val accountId = account.accountId - val archivedAccount = account.copy(isArchived = true) - val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + val updatedAccountList = (accountList - account).getOrNull()!! val exception = IllegalStateException("Save failed") diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt new file mode 100644 index 0000000000..eb0019f93c --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt @@ -0,0 +1,158 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetArchivedAccountsUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetArchivedAccountsUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should emit archived accounts when repository returns data`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption() + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf(archivedAccounts.lceContent()) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + + coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) } + } + + @Test + fun `invoke should emit loading and fetch when accounts not found`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Fetch error") + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow() + coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + exception.lceError(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun TestScope.getEmittedValues(flow: Flow): List { + val values = mutableListOf() + + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(values) + } + + return values + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt new file mode 100644 index 0000000000..4c994aeb21 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.account.usecase + +import arrow.core.left +import com.google.common.truth.Truth +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetUnoccupiedAccountIndexUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetUnoccupiedAccountIndexUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should return next unoccupied index when repository returns count`() = runTest { + // Arrange + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3 + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = DerivationIndex(4) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } + + @Test + fun `invoke should return error if repository throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } throws exception + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index 316e789754..7c8f1a847f 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -43,15 +43,14 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val archivedAccount = ArchivedAccount( accountId = account.accountId, - name = account.name, + name = account.accountName, icon = account.icon, derivationIndex = account.derivationIndex, tokensCount = 1, networksCount = 1, ) - val recoveredAccount = account.copy(isArchived = false) - val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val updatedAccountList = (accountList + account).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() @@ -60,7 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest { val actual = useCase(account.accountId) // Assert - val expected = recoveredAccount.right() + val expected = account.right() Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { @@ -173,15 +172,14 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val archivedAccount = ArchivedAccount( accountId = account.accountId, - name = account.name, + name = account.accountName, icon = account.icon, derivationIndex = account.derivationIndex, tokensCount = 1, networksCount = 1, ) - val recoveredAccount = account.copy(isArchived = false) - val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val updatedAccountList = (accountList + account).getOrNull()!! val exception = IllegalStateException("Save failed") coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index d6638b5c05..f1ba896a7c 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -73,7 +73,7 @@ class UpdateCryptoPortfolioUseCaseTest { value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) - val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon) + val updatedAccount = accountList.mainAccount.copy(icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -102,7 +102,7 @@ class UpdateCryptoPortfolioUseCaseTest { value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) - val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt index 597d6aa059..9f452c4145 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -1,7 +1,5 @@ package com.tangem.domain.account.utils -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWalletId import kotlin.random.Random @@ -33,13 +31,8 @@ fun createAccount( return Account.CryptoPortfolio( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = AccountName(name).getOrNull()!!, - accountIcon = icon, + icon = icon, derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) } \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt new file mode 100644 index 0000000000..2325c38b32 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt @@ -0,0 +1,13 @@ +package com.domain.blockaid.models.transaction.simultation + +import java.math.BigDecimal + +sealed class ApproveInfo { + data class Amount( + val approvedAmount: BigDecimal, + val isUnlimited: Boolean, + val tokenInfo: TokenInfo, + ) : ApproveInfo() + + data class NonFungibleToken(val name: String, val logoUrl: String?) : ApproveInfo() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt deleted file mode 100644 index 6fdad58ec2..0000000000 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.domain.blockaid.models.transaction.simultation - -import java.math.BigDecimal - -data class ApprovedAmount( - val approvedAmount: BigDecimal, - val isUnlimited: Boolean, - val tokenInfo: TokenInfo, -) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt index 13c28ebba4..13937ad93e 100644 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt @@ -16,9 +16,7 @@ sealed class SimulationData { /** * Represents an approve operation with the specified amount (can be multiple amounts for NFT) */ - data class Approve( - val approvedAmounts: List, - ) : SimulationData() + data class Approve(val items: List) : SimulationData() /** * Simulation was successfully performed and no changes detected diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt similarity index 86% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt index def2ff2645..40ce107753 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics internal sealed class AnalyticsParam { diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt similarity index 91% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 6115389bfd..0cb4a3685c 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt index c5c7e0ea3c..2398da3650 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt @@ -1,18 +1,14 @@ -package com.tangem.tap.common.analytics.converters +package com.tangem.domain.card.analytics import com.tangem.blockchain.common.Blockchain +import com.tangem.core.analytics.models.AnalyticsParam.WalletType import com.tangem.domain.card.CardTypesResolver -import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam -/** -[REDACTED_AUTHOR] - */ -class ParamCardCurrencyConverter : Converter { +class ParamCardCurrencyConverter : Converter { - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency + override fun convert(value: CardTypesResolver): WalletType? { + if (value.isMultiwalletAllowed()) return WalletType.MultiCurrency val type = when { value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) @@ -22,6 +18,6 @@ class ParamCardCurrencyConverter : Converter null } ?: return null - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) + return WalletType.SingleCurrency(type.value) } } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt similarity index 74% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt index 632cc66ef2..62c02a56ab 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -1,8 +1,8 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent -internal sealed class Shop( +sealed class Shop( event: String, params: Map = mapOf(), ) : AnalyticsEvent("Shop", event, params) { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt index f0f9ee988a..fbcfeb9a0c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt @@ -73,8 +73,21 @@ interface UserWalletsListRepository { * If the wallet is not found, it returns [SetLockError.UserWalletNotFound] * If the wallet is locked, it returns [SetLockError.UserWalletLocked] * If the lock method is not supported, it returns [SetLockError.UnableToSetLock]. + * + * @param userWalletId The ID of the user wallet to set the lock for. + * @param lockMethod The method to use for locking the wallet. + * @param changeUnsecured If false, the method will have no effect on unsecured wallets. */ - suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either + suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean = true, + ): Either + + /** + * Removes biometric lock for user wallet if it is set. + */ + suspend fun removeBiometricLock(userWalletId: UserWalletId) /** * Deletes user wallets by ids. diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt new file mode 100644 index 0000000000..24c29fd411 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SaveFirstColdWalletError { + data object CreateWalletError : SaveFirstColdWalletError + data class SaveError(val error: SaveWalletError) : SaveFirstColdWalletError + data class SelectError(val error: SelectWalletError) : SaveFirstColdWalletError +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 3ac8a33c7f..087b4312ed 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -7,11 +7,13 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.transaction.models.AssetRequirementsCondition import kotlinx.coroutines.flow.Flow /** * Manager that holds info about available actions as Sell and Buy */ +@Deprecated("Move to express domain layer") interface RampStateManager { suspend fun availableForBuy(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): ScenarioUnavailabilityReason @@ -50,4 +52,9 @@ interface RampStateManager { userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): ScenarioUnavailabilityReason + + /** + * Returns whether asset requirements are full filled to be able use express services + */ + fun checkAssetRequirements(requirements: AssetRequirementsCondition?): Boolean } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt index e8a4b8d205..6a9c3a47ed 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt @@ -10,6 +10,7 @@ class GetManagedTokensUseCase( operator fun invoke( context: ManageTokensListBatchingContext, + // only for onboarding case, change carefully and check repository implementation loadUserTokensFromRemote: Boolean, batchSize: Int = 40, ): ManageTokensListBatchFlow { diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt index f1c80f3ba0..044bc6a726 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -6,9 +6,13 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync class FilterAvailableNetworksForWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val excludedBlockchains: ExcludedBlockchains, ) { @@ -20,7 +24,7 @@ class FilterAvailableNetworksForWalletUseCase( userWalletId: UserWalletId, networks: Set, ): Set { - val userWallet = userWalletsListManager.userWalletsSync.firstOrNull { + val userWallet = getWallets().firstOrNull { it.walletId == userWalletId } ?: return networks.toSet() @@ -33,4 +37,10 @@ class FilterAvailableNetworksForWalletUseCase( supportedBlockchains.contains(blockchain) }.toSet() } + + private fun getWallets() = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index db4a5b164c..b376be0db1 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -1,9 +1,8 @@ package com.tangem.domain.models.account import arrow.core.Either +import arrow.core.getOrElse import arrow.core.raise.either -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError import com.tangem.domain.models.currency.CryptoCurrency @@ -22,7 +21,7 @@ sealed interface Account { val accountId: AccountId /** Name of the account */ - val name: AccountName + val accountName: AccountName /** The identifier of the user wallet associated with the account */ val userWalletId: UserWalletId @@ -31,21 +30,19 @@ sealed interface Account { /** * Represents a crypto portfolio account * - * @property accountId unique identifier of the account - * @property name name of the account - * @property icon icon representing the account - * @property derivationIndex index used for derivation of the account - * @property isArchived indicates whether the account is archived - * @property cryptoCurrencyList list of tokens associated with the account + * @property accountId unique identifier of the account + * @property accountName name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property cryptoCurrencies set of tokens associated with the account */ @Serializable data class CryptoPortfolio private constructor( override val accountId: AccountId, - override val name: AccountName, + override val accountName: AccountName, val icon: CryptoPortfolioIcon, val derivationIndex: DerivationIndex, - val isArchived: Boolean, - val cryptoCurrencyList: CryptoCurrencyList, + val cryptoCurrencies: Set, ) : Account { /** Indicates if the account is the main account */ @@ -54,41 +51,22 @@ sealed interface Account { /** Number of tokens in the account */ val tokensCount: Int - get() = cryptoCurrencyList.currencies.size + get() = cryptoCurrencies.size /** Number of distinct networks in the account */ val networksCount: Int - get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size - fun copy( - accountName: AccountName = this.name, - accountIcon: CryptoPortfolioIcon = this.icon, - isArchived: Boolean = this.isArchived, - ): CryptoPortfolio { + fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio { return CryptoPortfolio( accountId = this.accountId, - name = accountName, - icon = accountIcon, + accountName = accountName, + icon = icon, derivationIndex = this.derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = this.cryptoCurrencyList, + cryptoCurrencies = this.cryptoCurrencies, ) } - /** - * Represents a list of tokens in the account - * - * @property currencies set of cryptocurrencies in the account - * @property sortType sorting type for the tokens - * @property groupType grouping type for the tokens - */ - @Serializable - data class CryptoCurrencyList( - val currencies: Set, - val sortType: TokensSortType, - val groupType: TokensGroupType, - ) - /** * Represents possible errors when creating a crypto portfolio account */ @@ -109,33 +87,34 @@ sealed interface Account { /** * Constructor for creating a [CryptoPortfolio] instance * - * @param accountId unique identifier of the account - * @param name name of the account - * @param accountIcon icon representing the account - * @param derivationIndex index used for derivation of the account - * @param isArchived indicates whether the account is archived - * @param cryptoCurrencyList list of tokens associated with the account + * @param accountId unique identifier of the account + * @param name name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account */ - @Suppress("LongParameterList") operator fun invoke( accountId: AccountId, name: String, - accountIcon: CryptoPortfolioIcon, + icon: CryptoPortfolioIcon, derivationIndex: Int, - isArchived: Boolean, - cryptoCurrencyList: CryptoCurrencyList, + cryptoCurrencies: Set = emptySet(), ): Either { return either { - val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind() - val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind() + val accountName = AccountName(value = name).getOrElse { + raise(AccountNameError(cause = it)) + } + + val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse { + raise(DerivationIndexError(cause = it)) + } invoke( accountId = accountId, accountName = accountName, - accountIcon = accountIcon, + icon = icon, derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, + cryptoCurrencies = cryptoCurrencies, ) } } @@ -143,38 +122,39 @@ sealed interface Account { /** * Constructor for creating a [CryptoPortfolio] instance * - * @param accountId unique identifier of the account - * @param accountName name of the account - * @param accountIcon icon representing the account - * @param derivationIndex index used for derivation of the account - * @param isArchived indicates whether the account is archived - * @param cryptoCurrencyList list of tokens associated with the account + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account */ @Suppress("LongParameterList") operator fun invoke( accountId: AccountId, accountName: AccountName, - accountIcon: CryptoPortfolioIcon, + icon: CryptoPortfolioIcon, derivationIndex: DerivationIndex, - isArchived: Boolean, - cryptoCurrencyList: CryptoCurrencyList, + cryptoCurrencies: Set = emptySet(), ): CryptoPortfolio { return CryptoPortfolio( accountId = accountId, - name = accountName, - icon = accountIcon, + accountName = accountName, + icon = icon, derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, + cryptoCurrencies = cryptoCurrencies, ) } /** * Creates a main account for the given user wallet ID * - * @param userWalletId the ID of the user wallet + * @param userWalletId the ID of the user wallet + * @param cryptoCurrencies set of tokens associated with the account */ - fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { + fun createMainAccount( + userWalletId: UserWalletId, + cryptoCurrencies: Set = emptySet(), + ): CryptoPortfolio { val derivationIndex = DerivationIndex.Main return CryptoPortfolio( @@ -182,15 +162,10 @@ sealed interface Account { userWalletId = userWalletId, derivationIndex = derivationIndex, ), - name = AccountName.Main, + accountName = AccountName.Main, icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = cryptoCurrencies, ) } } diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index eabb287672..91baaf2a76 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -1,10 +1,7 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio -import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -100,13 +97,12 @@ class AccountTest { val name = "" // Act - val actual = CryptoPortfolio( + val actual = CryptoPortfolio.invoke( accountId = mockk(), name = name, - accountIcon = mockk(), + icon = mockk(), derivationIndex = 0, - isArchived = false, - cryptoCurrencyList = mockk(), + cryptoCurrencies = emptySet(), ) .leftOrNull()!! @@ -125,14 +121,9 @@ class AccountTest { derivationIndex = derivationIndex, ), name = "Test Account", - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), derivationIndex = derivationIndex.value, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) .getOrNull()!! @@ -157,14 +148,9 @@ class AccountTest { derivationIndex = derivationIndex, ), accountName = AccountName.Main, - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) Truth.assertThat(actual).isEqualTo(expected) @@ -182,14 +168,9 @@ class AccountTest { return CryptoPortfolio.invoke( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex), name = name, - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = currencies, - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = currencies, ) .getOrNull()!! } diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 1a64989894..dd8608db95 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -24,8 +24,10 @@ interface SettingsRepository { suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun shouldSaveAccessCodes(): Boolean + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun setShouldSaveAccessCodes(value: Boolean) suspend fun incrementAppLaunchCounter() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index c1d7cd8cc8..042f4d257f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.transformLatest class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { @OptIn(ExperimentalCoroutinesApi::class) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 1d02262cc7..0fc94390be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -20,7 +20,7 @@ import timber.log.Timber import java.util.concurrent.ConcurrentHashMap class GetWalletTotalBalanceUseCase( - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { private val walletBalanceCache = ConcurrentHashMap() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index e39261d0a7..3a258c4cf6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -28,6 +29,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -56,6 +58,8 @@ abstract class BaseCurrencyStatusOperations( protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator() + abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> + protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> protected abstract suspend fun fetchComponents( @@ -382,8 +386,9 @@ abstract class BaseCurrencyStatusOperations( multiWalletCryptoCurrenciesSupplier.getSyncOrNull( params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), ) - ?.firstOrNull { it.network.id == networkId && it.network.derivationPath == derivationPath } - ?: error("Unable to create network coin with ID: $networkId and derivation path: $derivationPath") + ?.filterIsInstance() + ?.firstOrNull { it.network.id == networkId } + ?: error("Unable to create network coin with ID: $networkId") } else { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index fcce30d9fd..70700beea8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -60,19 +60,18 @@ class CachedCurrenciesStatusesOperations( multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, -) : BaseCurrenciesStatusesOperations, - BaseCurrencyStatusOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - tokensFeatureToggles = tokensFeatureToggles, - ) { +) : BaseCurrencyStatusOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + stakingIdFactory = stakingIdFactory, + tokensFeatureToggles = tokensFeatureToggles, +) { override fun getCurrenciesStatuses( userWalletId: UserWalletId, @@ -83,6 +82,7 @@ class CachedCurrenciesStatusesOperations( ) } + @Suppress("LongMethod") @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, @@ -163,10 +163,26 @@ class CachedCurrenciesStatusesOperations( .invokeOnCompletion { setFetchFinished(userWalletId) } } + val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) + combine( flow = getQuotes(currenciesIds), - flow2 = getNetworkStatusesUpdates(userWalletId, networks), - flow3 = getYieldsBalancesUpdates(userWalletId, currencies), + flow2 = networksStatusesUpdates, + flow3 = networksStatusesUpdates.flatMapLatest { maybeNetworksStatuses -> + val networksStatuses = maybeNetworksStatuses.getOrNull() + + val currenciesAddresses = if (networksStatuses == null) { + emptyMap() + } else { + currencies.associate { currency -> + val networkStatus = networksStatuses.firstOrNull { it.network == currency.network } + + currency.id to extractAddress(networkStatus) + } + } + + getYieldsBalancesUpdates(userWalletId, currenciesAddresses) + }, flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -379,24 +395,27 @@ class CachedCurrenciesStatusesOperations( // temporary code because token list is built using networks list private fun getYieldsBalancesUpdates( userWalletId: UserWalletId, - cryptoCurrencies: List, + cryptoCurrencies: Map, ): EitherFlow> { return channelFlow { val state = MutableStateFlow(emptyList()) - val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) + val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> + stakingIdFactory.create( + currencyId = currencyWithAddress.key, + defaultAddress = currencyWithAddress.value, + ) .getOrNull() } - stakingIds.onEach { + stakingIds.onEach { stakingId -> launch { singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = it), + params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) .onEach { balance -> state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { balance.stakingId == it } + loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId } } } .launchIn(scope = this) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 62feb67e90..f6171137cd 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -109,4 +109,18 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): com.tangem.blockchain.extensions.Result> + + suspend fun prepareAndSign( + transactionData: TransactionData, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): com.tangem.blockchain.extensions.Result + + suspend fun prepareAndSignMultiple( + transactionData: List, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): com.tangem.blockchain.extensions.Result> } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt new file mode 100644 index 0000000000..4fecd3147f --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt @@ -0,0 +1,70 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.SendTransactionError + +class PrepareAndSignUseCase( + private val transactionRepository: TransactionRepository, + private val cardSdkConfigRepository: CardSdkConfigRepository, +) { + + suspend operator fun invoke( + transactionData: TransactionData, + userWallet: UserWallet, + network: Network, + ): Either { + val signer = createSigner(userWallet) + val result = transactionRepository.prepareAndSign( + transactionData = transactionData, + userWalletId = userWallet.walletId, + network = network, + signer = signer, + ) + return when (result) { + is Result.Failure -> SendTransactionUseCase.handleError(result).left() + is Result.Success -> result.data.right() + } + } + + suspend operator fun invoke( + transactionData: List, + userWallet: UserWallet, + network: Network, + ): Either> { + val signer = createSigner(userWallet) + val result = transactionRepository.prepareAndSignMultiple( + transactionData = transactionData, + userWalletId = userWallet.walletId, + network = network, + signer = signer, + ) + return when (result) { + is Result.Failure -> SendTransactionUseCase.handleError(result).left() + is Result.Success -> result.data.right() + } + } + + private fun createSigner(userWallet: UserWallet): TransactionSigner { + userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + return signer + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt index 17de705186..d6c97a1919 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt @@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable @Serializable data class VisaDataToSignByCustomerWallet( - val request: VisaCustomerWalletDataToSignRequest, val hashToSign: String, + val request: VisaCustomerWalletDataToSignRequest? = null, ) fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) = diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt new file mode 100644 index 0000000000..7c46288fc1 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaSignedChallengeByCustomerWallet( + val challenge: String, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt index e072914e92..7d46ff2d52 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -3,13 +3,12 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.pay.KycStartInfo -import com.tangem.domain.models.wallet.UserWalletId interface KycRepository { - suspend fun getKycStartInfo(): Either + suspend fun getKycStartInfo(address: String, cardId: String): Either interface Factory { - fun create(userWalletId: UserWalletId): KycRepository + fun create(): KycRepository } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index f7f19ca0e9..098ca44c00 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -18,6 +18,16 @@ interface VisaAuthRepository { cardWalletAddress: String, ): Either + suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either + + suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either + suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt index 9daf8154a1..33633558c4 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt @@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WcEthAddChain( + /** + * chainId are identified by EIP-155 integers expressed in hexadecimal notation, + * with 0x prefix and no leading zeroes for the chainId value. + * For more information https://eips.ethereum.org/EIPS/eip-5792#atomicbatch-capability + */ @Json(name = "chainId") val chainId: String, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt index 24a4878ec4..34b43e11bb 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -1,7 +1,5 @@ package com.tangem.domain.walletconnect.model -import com.tangem.domain.models.network.Network - sealed interface WcEthMethod : WcMethod { data class MessageSign( @@ -15,7 +13,7 @@ sealed interface WcEthMethod : WcMethod { val account: String, val dataForSign: String, ) : WcEthMethod { - val humanMsg: String = params.message.contents.orEmpty() + val humanMsg: String = params.message?.contents.orEmpty() } data class SendTransaction( @@ -28,6 +26,9 @@ sealed interface WcEthMethod : WcMethod { data class AddEthereumChain( val rawChain: WcEthAddChain, - val network: Network, + ) : WcEthMethod + + data class SwitchEthereumChain( + val rawChain: WcEthAddChain, ) : WcEthMethod } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt index 0054a9c68c..b14e6af6df 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt @@ -6,24 +6,24 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WcEthSignTypedDataParams( @Json(name = "domain") - val domain: Domain, + val domain: Domain?, @Json(name = "message") - val message: Message, + val message: Message?, @Json(name = "primaryType") - val primaryType: String, + val primaryType: String?, @Json(name = "types") val types: Map>, ) { @JsonClass(generateAdapter = true) data class Domain( @Json(name = "chainId") - val chainId: Int, + val chainId: Int?, @Json(name = "name") - val name: String, + val name: String?, @Json(name = "verifyingContract") - val verifyingContract: String, + val verifyingContract: String?, @Json(name = "version") - val version: String, + val version: String?, ) @JsonClass(generateAdapter = true) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt index 6ae4b431ac..982c1cacf1 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt @@ -14,6 +14,7 @@ enum class WcEthMethodName(override val raw: String) : WcMethodName { SignTransaction("eth_signTransaction"), SendTransaction("eth_sendTransaction"), AddEthereumChain("wallet_addEthereumChain"), + SwitchEthereumChain("wallet_switchEthereumChain"), } enum class WcSolanaMethodName(override val raw: String) : WcMethodName { diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt index f04de6251a..2ac1b68531 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -16,4 +16,5 @@ sealed class WcPairError( data class ApprovalFailed(override val message: String) : WcPairError("107 002 003") data object RejectionFailed : WcPairError("107 002 004") data class Unknown(override val message: String) : WcPairError(message) + data class TimeoutException(override val message: String) : WcPairError(message) } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt index 3c1cfe610a..e0ad853f77 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt @@ -69,4 +69,9 @@ sealed class HandleMethodError( data object UnknownSession : HandleMethodError(message = "WalletConnect session was disconnected") data class UnknownError(override val message: String) : HandleMethodError(message) + data class TangemUnsupportedNetwork(val unsupportedNetwork: String) : + HandleMethodError("TangemUnsupportedNetwork $unsupportedNetwork") + + data class NotAddedNetwork(val networkName: String) : HandleMethodError("NotAddedNetwork $networkName") + data class RequiredNetwork(val networkName: String) : HandleMethodError("RequiredNetwork $networkName") } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt index 0042025dfb..d44d98c88f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -2,18 +2,27 @@ package com.tangem.domain.walletconnect.model sealed interface WcSolanaMethod : WcMethod { + val methodName: String + val trimmedPrefixMethodName: String get() = methodName.substringAfter("_") + data class SignMessage( val pubKey: String, val rawMessage: String, val humanMsg: String, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignMessage.raw + } data class SignTransaction( val transaction: String, val address: String?, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignTransaction.raw + } data class SignAllTransaction( val transaction: List, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SendAllTransaction.raw + } } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt index 9b2d6c45b7..63ea82098f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt @@ -6,4 +6,12 @@ package com.tangem.domain.walletconnect.model.sdkcopy data class WcSdkSession( val topic: String, val appMetaData: WcAppMetaData, -) \ No newline at end of file + val namespaces: Map, +) { + data class Session( + val chains: List, + val accounts: List, + val methods: List, + val events: List, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 0d3291dca3..8b6bd34eae 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -11,6 +11,7 @@ import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.extensions.mapNotNullValues sealed class WcAnalyticEvents( event: String, @@ -103,7 +104,7 @@ sealed class WcAnalyticEvents( class SignatureRequestReceived( rawRequest: WcSdkSessionRequest, network: Network, - emulationStatus: EmulationStatus, + emulationStatus: EmulationStatus?, ) : WcAnalyticEvents( event = "Signature Request Received", params = mapOf( @@ -111,8 +112,8 @@ sealed class WcAnalyticEvents( AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url, AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method, AnalyticsParam.Key.BLOCKCHAIN to network.name, - AnalyticsParam.Key.EMULATION_STATUS to emulationStatus.status, - ), + AnalyticsParam.Key.EMULATION_STATUS to emulationStatus?.status, + ).mapNotNullValues { it.value }, ) { enum class EmulationStatus(val status: String) { Emulated("Emulated"), diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt index 787bea6e93..7573141b6d 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt @@ -1,12 +1,20 @@ package com.tangem.domain.walletconnect.usecase.method import arrow.core.Either +import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.HandleMethodError import com.tangem.domain.walletconnect.model.WcRequestError interface WcAddNetworkUseCase : WcMethodUseCase, WcMethodContext { + suspend operator fun invoke(): Either suspend fun approve(): Either fun reject() + + data class AddNetwork( + val network: Network, + val isExistInWcSession: Boolean, + ) } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt new file mode 100644 index 0000000000..b7980f5500 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletconnect.usecase.method + +import arrow.core.Either +import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.HandleMethodError + +interface WcSwitchNetworkUseCase : + WcMethodUseCase, + WcMethodContext { + + suspend operator fun invoke(): Either + fun reject() + + data class SwitchNetwork( + val network: Network, + val isExistInWcSession: Boolean, + ) +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 8fbf7b49dd..a467663bf5 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -37,6 +37,11 @@ dependencies { implementation(tangemDeps.hot.core) // endregion + /** Other libraries */ + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.timber) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index 4707a09f81..eaa1c7dc7c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet + val allNetworks = Blockchain.entries.filter { it.isTestnet().not() } val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt new file mode 100644 index 0000000000..103f85021a --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO + +class ColdCurvesConfig(cardDTO: CardDTO) : CurvesConfig { + + val cardConfig = CardConfig.createConfig(cardDTO) + + override val mandatoryCurves: List + get() = cardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return cardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt new file mode 100644 index 0000000000..dcf753e3e3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.models.wallet.UserWallet + +interface CurvesConfig { + + val mandatoryCurves: List + + fun primaryCurve(blockchain: Blockchain): EllipticCurve? +} + +val UserWallet.curvesConfig: CurvesConfig + get() = when (this) { + is UserWallet.Cold -> ColdCurvesConfig(this.scanResponse.card) + is UserWallet.Hot -> HotCurvesConfig + } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt new file mode 100644 index 0000000000..eec380f63d --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.Wallet2CardConfig + +data object HotCurvesConfig : CurvesConfig { + + override val mandatoryCurves: List + get() = Wallet2CardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return Wallet2CardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index 16b0be7fa0..403ac7a76b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -10,11 +10,14 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class DefaultUserWalletsSyncDelegate( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val dispatchers: CoroutineDispatcherProvider, ) : UserWalletsSyncDelegate { @@ -28,10 +31,43 @@ class DefaultUserWalletsSyncDelegate( } } - // TODO remove dispatchers whnen UserWalletsListManager will be main safe private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, + ): Either = if (useNewRepository) { + renameUserWalletInNewRepository(userWalletId, name) + } else { + renameUserWalletInLegacyRepository(userWalletId, name) + } + + private suspend fun renameUserWalletInNewRepository( + userWalletId: UserWalletId, + name: String, + ): Either = either { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.find { it.walletId == userWalletId } + ?: raise(UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found"))) + + ensure(userWallets.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + ensure(name != userWallet.name) { + UpdateWalletError.NameAlreadyExists + } + + val updatedWallet = userWallet.copy(name = name) + + userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .map { updatedWallet } + .mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("")) } + .bind() + } + + // TODO remove dispatchers whnen UserWalletsListManager will be main safe + private suspend fun renameUserWalletInLegacyRepository( + userWalletId: UserWalletId, + name: String, ): Either = withContext(dispatchers.io) { either { val existingNames = userWalletsListManager.userWalletsSync diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt index 25d25bd977..74055cd741 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt @@ -4,17 +4,14 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.common.util.hasDerivation -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.wallet.UserWallet -import kotlin.collections.first -import kotlin.collections.orEmpty +import com.tangem.domain.wallets.config.curvesConfig fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Boolean { return when (this) { is UserWallet.Cold -> scanResponse.hasDerivation(blockchain, derivationPath) is UserWallet.Hot -> { - // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet - val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) + val primaryCurve = curvesConfig.primaryCurve(blockchain) val list = if (blockchain == Blockchain.Cardano) { listOf( CardanoUtils.extendedDerivationPath(DerivationPath(derivationPath)), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..48c37f6440 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.wallets.hot + +import com.tangem.hot.sdk.model.HotWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository for managing access code attempts for hot wallets. + * It tracks the number of attempts made to access a hot wallet and applies cooldowns or deletion + * based on the number of attempts. + */ +interface HotWalletAccessCodeAttemptsRepository { + + /** + * Increments the number of attempts for the given [AttemptId]. + * If the number of attempts exceeds [MAX_FAST_FORWARD_ATTEMPTS], a cooldown period is initiated. + */ + suspend fun incrementAttempts(id: AttemptId) + + /** + * Resets the attempts for the given [HotWalletId]. + * This is typically called when the user successfully authenticates or when the wallet is deleted. + */ + suspend fun resetAttempts(hotWalletId: HotWalletId) + + /** + * Retrieves the current attempts for the given [AttemptId]. + * The result is a flow that emits the current state of attempts. + */ + fun getAttempts(id: AttemptId): Flow + + /** + * Synchronously retrieves the current attempts for the given [AttemptId]. + * This is useful when you need to get the attempts without using a flow. + */ + suspend fun getAttemptsSync(id: AttemptId): Attempts + + data class AttemptId( + val hotWalletId: HotWalletId, + val auth: Boolean, + ) + + sealed interface Attempts { + val count: Int + + data class FastForward( + override val count: Int, + ) : Attempts + + data class WithDelay( + override val count: Int, + val remainingSeconds: Int, + ) : Attempts + + data class BeforeDeletion( + override val count: Int, + val remainingSeconds: Int, + val remainingAttemptsCountBeforeDeletion: Int, + ) : Attempts + + data object Deletion : Attempts { + override val count: Int = MAX_ATTEMPTS_BEFORE_DELETION + } + } + + companion object { + const val COOLDOWN_SECONDS = 60 + const val MAX_FAST_FORWARD_ATTEMPTS = 5 + const val ATTEMPTS_BEFORE_DELETION = 20 + const val MAX_ATTEMPTS_BEFORE_DELETION = 30 + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index 7dcc5fa579..c3f3df8d95 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -1,17 +1,49 @@ package com.tangem.domain.wallets.hot import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId +/** + * Interface for requesting the password for a hot wallet. + * It provides methods to handle password requests, authentication states, and user interactions. + */ interface HotWalletPasswordRequester { + /** + * Sets state to show wrong password state. + */ suspend fun wrongPassword() + /** + * Sets state to show successful authentication state. + */ suspend fun successfulAuthentication() - suspend fun requestPassword(hasBiometry: Boolean): Result + /** + * Requests the user to enter the password for the hot wallet. + * @param attemptRequest Contains information about the hot wallet and authentication mode. + * @return Result of the password request, which can be either a password entry, biometric use, or dismissal. + */ + suspend fun requestPassword(attemptRequest: AttemptRequest): Result + /** + * Dismisses the password request dialog. + */ suspend fun dismiss() + /** + * Represents a request to authenticate with a hot wallet. + * @param hotWalletId The ID of the hot wallet to authenticate with. + * @param authMode Indicates whether the request is for authentication mode. + * In auth mode user can be deleted after failed attempts. + * @param hasBiometry Indicates whether to show biometric authentication option. + */ + data class AttemptRequest( + val hotWalletId: HotWalletId, + val authMode: Boolean, + val hasBiometry: Boolean, + ) + sealed class Result { data object UseBiometry : Result() data object Dismiss : Result() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt deleted file mode 100644 index e2aeab608f..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed interface SelectWalletError { - - object UnableToSelectUserWallet : SelectWalletError -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 5a6c051057..a1eb1f0cd8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -11,10 +11,20 @@ interface WalletsRepository { suspend fun shouldSaveUserWalletsSync(): Boolean + @Deprecated("Hot wallet make always save user wallets. Do not use this method") fun shouldSaveUserWallets(): Flow + @Deprecated("Hot wallet make always save user wallets. Do not use this method") suspend fun saveShouldSaveUserWallets(item: Boolean) + suspend fun useBiometricAuthentication(): Boolean + + suspend fun setUseBiometricAuthentication(value: Boolean) + + suspend fun requireAccessCode(): Boolean + + suspend fun setRequireAccessCode(value: Boolean) + suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index c876b7d526..c3c49c4f7e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.error.DeleteWalletError import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for deleting user wallet @@ -14,7 +15,11 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class DeleteWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { /** * Deletes user wallet with provided ID. @@ -24,6 +29,12 @@ class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListMan * @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. * */ suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map { + userWalletsListRepository.selectedUserWallet.value != null + } + } + return either { userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) .doOnFailure { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt new file mode 100644 index 0000000000..7c80707cb6 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.wallets.usecase + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class GenerateBuyTangemCardLinkUseCase { + + suspend operator fun invoke(): String = suspendCoroutine { cont -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { id -> + cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id") + } + .addOnFailureListener { + cont.resume(NEW_BUY_WALLET_URL) + } + } + + companion object { + private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt index e779085950..fdc88856e7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -2,12 +2,16 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.scan.ProductType import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for user wallet name generation */ class GenerateWalletNameUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { @@ -17,16 +21,24 @@ class GenerateWalletNameUseCase( isStartToCoin = isStartToCoin, ) - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } fun invokeForHot(): String { val defaultName = "Wallet" - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } + private fun getNamesSet(): Set { + return if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet() + } else { + userWalletsListManager.userWalletsSync.map { it.name }.toSet() + } + } + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { val startIndex = 2 if (!existingNames.contains(defaultName)) { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt deleted file mode 100644 index b57e8d3ec8..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.sdk.api.TangemSdkManager -import javax.inject.Inject - -class GetIsBiometricsEnabledUseCase @Inject constructor( - private val tangemSdkManager: TangemSdkManager, -) { - - operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt index 6afc79d552..32233cdbf9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt @@ -4,13 +4,20 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.* class GetSavedWalletsCountUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(): Flow> { + if (useNewRepository) { + return userWalletsListRepository.userWallets.map { requireNotNull(it) } + } + return userWalletsListManager.savedWalletsCount .filter { count -> if (count == 0) return@filter true diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index 00451b45de..5b681fb679 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for getting selected wallet. @@ -15,10 +16,20 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetSelectedWalletSyncUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either { + if (useNewRepository) { + return either { + userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound) + } + } + return either { ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index d479a9d59b..57a01d23fb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -5,7 +5,9 @@ import arrow.core.raise.either import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull /** * Use case for getting flow of selected wallet. @@ -14,12 +16,32 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") +class GetSelectedWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either> { return either { - userWalletsListManager.selectedUserWallet + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.filterNotNull() + } else { + userWalletsListManager.selectedUserWallet + } + } + } + + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") + fun sync(): Either { + return either { + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.value + } else { + userWalletsListManager.selectedUserWalletSync + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index b1a548af9a..6f4e13f0f0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -10,13 +10,24 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest -class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetUserWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWallets = userWalletsListManager.userWalletsSync + val userWallets = if (useNewListRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) @@ -25,7 +36,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa @OptIn(ExperimentalCoroutinesApi::class) fun invokeFlow(userWalletId: UserWalletId): EitherFlow { - return userWalletsListManager.userWallets.transformLatest { userWallets -> + val flow = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } + + return flow.transformLatest { userWallets -> userWallets.firstOrNull { it.walletId == userWalletId } ?.let { emit(it.right()) } ?: emit(GetUserWalletError.UserWalletNotFound.left()) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt index 0108e03b67..377bf1b152 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -1,13 +1,23 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for getting list of user wallets names. * * @property userWalletsListManager user wallets list manager */ -class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletNamesUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { - operator fun invoke(): List = userWalletsListManager.userWalletsSync.map { it.name } + operator fun invoke(): List = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name } + } else { + userWalletsListManager.userWalletsSync.map { it.name } + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 7e6a0b6510..6635d63099 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -1,8 +1,10 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Use case for getting list of user wallets @@ -11,11 +13,23 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletsUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = userWalletsListManager.userWallets + operator fun invoke(): Flow> = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } @Throws(IllegalArgumentException::class) - fun invokeSync(): List = userWalletsListManager.userWalletsSync + fun invokeSync(): List = if (useNewListRepository) { + userWalletsListRepository.userWallets.value!! + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt index 29240ff71b..05d47b70d0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.UserWalletsListManager 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.map @@ -12,12 +13,22 @@ import kotlinx.coroutines.flow.map * * @property userWalletsListManager user wallets list manager */ -class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) { +class IsNeedToBackupUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { operator fun invoke(id: UserWalletId): Flow { - return userWalletsListManager.userWallets + val userWalletsFlow = if (useNewRepository) { + userWalletsListRepository.userWallets + } else { + userWalletsListManager.userWallets + } + + return userWalletsFlow .map { wallets -> - val wallet = wallets.firstOrNull { it.walletId == id } + val wallet = wallets?.firstOrNull { it.walletId == id } if (wallet == null) { false } else { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 9ff8c5bb81..7e328ae146 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -6,10 +6,12 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletsRepository /** * Use case for saving user wallet @@ -18,22 +20,60 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class SaveWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { - return either { - userWalletsListManager.save(userWallet, canOverride) - .doOnSuccess { return Unit.right() } - .doOnFailure { - return when (it) { - is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( - it.messageResId, - ) - else -> SaveWalletError.DataError(it.messageResId) - }.left() - } + return if (useNewRepository) { + either { + val newUserWallet = + userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() - return Unit.right() + if (newUserWallet) { + when (userWallet) { + is UserWallet.Cold -> { + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } else { + Unit.right() + } + } + is UserWallet.Hot -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.NoLock, + ) + } + }.mapLeft { + SaveWalletError.DataError(null) + }.map { + userWalletsListRepository.select(userWallet.walletId) + }.bind() + } + } + } else { + either { + userWalletsListManager.save(userWallet, canOverride) + .doOnSuccess { return Unit.right() } + .doOnFailure { + return when (it) { + is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( + it.messageResId, + ) + else -> SaveWalletError.DataError(it.messageResId) + }.left() + } + + return Unit.right() + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 3ff5b201d7..21a6a76755 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -4,11 +4,12 @@ import arrow.core.Either import arrow.core.raise.either import arrow.core.right import com.tangem.common.CompletionResult +import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for selecting wallet @@ -20,10 +21,19 @@ import com.tangem.domain.models.wallet.UserWalletId */ class SelectWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.select(userWalletId).map { + reduxStateHolder.onUserWalletSelected(it) + it + } + } + return either { return when (val result = userWalletsListManager.select(userWalletId)) { is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index b6d0accf29..96c2f19f7a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -7,6 +7,9 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.wallets.models.UpdateWalletError.* +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for updating user wallet @@ -15,15 +18,38 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class UpdateWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, - ): Either = either { - when (val result = userWalletsListManager.update(userWalletId, update)) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data + ): Either { + if (useNewRepository) { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + ?: return Either.Left( + UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found")), + ) + val updatedWallet = update(userWallet) + return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .mapLeft { + when (it) { + is SaveWalletError.DataError -> DataError( + IllegalStateException("Failed to update wallet: ${it.messageId}"), + ) + is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists + } + } + } + + return either { + when (val result = userWalletsListManager.update(userWalletId, update)) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt index b71be78c5e..4df283dd5e 100644 --- a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt @@ -22,7 +22,11 @@ class GetSavedWalletsCountUseCaseTest { @Before fun setup() { userWalletsListManager = mockk() - useCase = GetSavedWalletsCountUseCase(userWalletsListManager) + useCase = GetSavedWalletsCountUseCase( + userWalletsListManager, + userWalletsListRepository = mockk(), + useNewRepository = false, + ) mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") } diff --git a/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt new file mode 100644 index 0000000000..91acb0ea4d --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface ArchivedAccountListComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts index e68d20b144..dc77a38d28 100644 --- a/features/account/impl/build.gradle.kts +++ b/features/account/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.utils) implementation(projects.core.ui) + implementation(projects.core.error) implementation(projects.core.res) implementation(projects.core.decompose) implementation(projects.core.navigation) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt new file mode 100644 index 0000000000..a93cdcca2b --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -0,0 +1,68 @@ +package com.tangem.features.account.archived + +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("UnusedPrivateMember") // todo account +internal class ArchivedAccountListModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow get() = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun confirmRecoverDialog(accountId: AccountId) { + val account: Account? = null // todo account find + account ?: return + val secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_archived_recover), + onClick = { recoverCryptoPortfolio(account.accountId) }, + ) + messageSender.send( + DialogMessage( + title = stringReference(account.accountName.value), + message = TextReference.EMPTY, + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + recoverCryptoPortfolioUseCase(accountId) + } + + private fun getInitialState(): AccountArchivedUM { + return AccountArchivedUM.Loading( + onCloseClick = { router.pop() }, + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt new file mode 100644 index 0000000000..6179fd12b2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.archived + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ui.ArchivedAccountListContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultArchivedAccountListComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: ArchivedAccountListComponent.Params, +) : AppComponentContext by appComponentContext, ArchivedAccountListComponent { + + private val model: ArchivedAccountListModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ArchivedAccountListContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : ArchivedAccountListComponent.Factory { + override fun create( + context: AppComponentContext, + params: ArchivedAccountListComponent.Params, + ): DefaultArchivedAccountListComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt new file mode 100644 index 0000000000..21c674cef2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ArchivedAccountListModel +import com.tangem.features.account.archived.DefaultArchivedAccountListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountArchivedModule { + + @Binds + fun bindArchivedAccountListComponentFactory( + impl: DefaultArchivedAccountListComponent.Factory, + ): ArchivedAccountListComponent.Factory + + @Binds + @IntoMap + @ClassKey(ArchivedAccountListModel::class) + fun bindArchivedAccountListModel(model: ArchivedAccountListModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt new file mode 100644 index 0000000000..bd72960062 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.entity + +import com.tangem.common.ui.account.CryptoPortfolioIconUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal sealed interface AccountArchivedUM { + val onCloseClick: () -> Unit + + data class Loading(override val onCloseClick: () -> Unit) : AccountArchivedUM + data class Error( + override val onCloseClick: () -> Unit, + val onRetryClick: () -> Unit, + ) : AccountArchivedUM + data class Content( + override val onCloseClick: () -> Unit, + val accounts: ImmutableList, + ) : AccountArchivedUM +} + +internal data class ArchivedAccountUM( + val accountId: String, + val accountName: TextReference, + val accountIconUM: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val onClick: (accountId: String) -> Unit, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt new file mode 100644 index 0000000000..a563f952d3 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -0,0 +1,182 @@ +package com.tangem.features.account.archived.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.CircularProgressIndicator +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 androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountRow +import com.tangem.core.res.R +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.features.account.archived.entity.ArchivedAccountUM +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun ArchivedAccountListContent(state: AccountArchivedUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = stringResourceSafe(R.string.account_archived_title), + onBackClick = state.onCloseClick, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + + ) { + when (state) { + is AccountArchivedUM.Content -> ArchiveAccountContent(state) + is AccountArchivedUM.Error -> ArchiveAccountError(state) + is AccountArchivedUM.Loading -> ArchiveAccountLoading() + } + } + } +} + +@Composable +private fun ArchiveAccountLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + modifier = Modifier, + ) + } +} + +@Composable +private fun ArchiveAccountError(state: AccountArchivedUM.Error, modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = stringResourceSafe(R.string.common_unable_to_load), + ) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = state.onRetryClick, + ), + ) + } + } +} + +@Composable +private fun ArchiveAccountContent(state: AccountArchivedUM.Content, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = state.accounts, + key = { index, item -> item.accountId }, + ) { index, account -> + ArchivedAccountRow( + item = account, + modifier = Modifier.roundedShapeItemDecoration( + backgroundColor = TangemTheme.colors.background.primary, + radius = TangemTheme.dimens.radius20, + currentIndex = index, + addDefaultPadding = true, + lastIndex = state.accounts.lastIndex, + ), + ) + } + } +} + +@Composable +private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = { item.onClick(item.accountId) }) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountRow( + title = item.accountName, + subtitle = item.tokensInfo, + icon = item.accountIconUM, + modifier = Modifier.weight(1f), + ) + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.account_archived_recover), + onClick = { item.onClick(item.accountId) }, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountArchivedUM) { + TangemThemePreview { + ArchivedAccountListContent(state = params) + } +} + +@Suppress("MagicNumber") +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + fun portfolioIcon() = AccountIconPreviewData.randomAccountIcon() + val accountName = stringReference("Account name") + + val firstList = List(10) { + ArchivedAccountUM( + accountId = it.toString(), + accountName = accountName, + accountIconUM = portfolioIcon(), + tokensInfo = stringReference("10 tokens in 2 networks"), + onClick = {}, + + ) + }.toImmutableList() + val first = AccountArchivedUM.Content( + onCloseClick = {}, + accounts = firstList, + ) + add(first) + add(AccountArchivedUM.Loading {}) + add(AccountArchivedUM.Error({}, {})) + }, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index b4763b9c92..5c61ebc3f4 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,5 +1,8 @@ package com.tangem.features.account.createedit +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.common.ui.account.toDomain import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,27 +12,34 @@ import com.tangem.core.res.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.utils.showErrorDialog import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent -import com.tangem.features.account.common.toDomain import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateDerivationIndex import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateIconSelect import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName +import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped +@Suppress("LongParameterList") internal class AccountCreateEditModel @Inject constructor( paramsContainer: ParamsContainer, private val messageSender: UiMessageSender, @@ -37,13 +47,21 @@ internal class AccountCreateEditModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val updateCryptoPortfolioUseCase: UpdateCryptoPortfolioUseCase, private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, + private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { private val params = paramsContainer.require() private val umBuilder = AccountCreateEditUMBuilder(params) - val uiState: StateFlow get() = _uiState - private val _uiState = MutableStateFlow(value = getInitialState()) + val uiState: StateFlow + field = MutableStateFlow(value = getInitialState()) + + init { + if (params is AccountCreateEditComponent.Params.Create) { + updateDerivationInfo(userWalletId = params.userWalletId) + } + } private fun unsaveChangeDialog() { val secondAction = EventMessageAction( @@ -74,13 +92,16 @@ internal class AccountCreateEditModel @Inject constructor( private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) { val state = uiState.value - val name = AccountName(state.account.name).getOrNull() ?: return + val name = AccountName(value = state.account.name).getOrNull() ?: return val icon = state.account.portfolioIcon.toDomain() + val index = state.account.derivationInfo.index ?: return + val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return + addCryptoPortfolioUseCase( userWalletId = params.userWalletId, accountName = name, icon = icon, - derivationIndex = DerivationIndex.Main, // todo account + derivationIndex = derivationIndex, ) } @@ -88,7 +109,7 @@ internal class AccountCreateEditModel @Inject constructor( val state = uiState.value val name = AccountName(state.account.name).getOrNull() ?: return val icon = state.account.portfolioIcon.toDomain() - val isNewName = name != params.account.name + val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon updateCryptoPortfolioUseCase( icon = if (isNewIcon) icon else null, @@ -100,19 +121,19 @@ internal class AccountCreateEditModel @Inject constructor( private fun onCloseClick() = unsaveChangeDialog() private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateIconSelect(icon) .validateNewState() } private fun onColorSelect(color: CryptoPortfolioIcon.Color) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateColorSelect(color) .validateNewState() } private fun onNameChange(name: String) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateName(name) .validateNewState() } @@ -122,7 +143,7 @@ internal class AccountCreateEditModel @Inject constructor( val isAvailableForConfirm = when (params) { is AccountCreateEditComponent.Params.Create -> isValidName is AccountCreateEditComponent.Params.Edit -> { - val isNewName = this.account.name != params.account.name.value + val isNewName = this.account.name != params.account.accountName.value val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon isValidName && (isNewName || isNewIcon) } @@ -140,4 +161,35 @@ internal class AccountCreateEditModel @Inject constructor( onCloseClick = ::onCloseClick, ) } + + private fun updateDerivationInfo(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.default) { + getUnoccupiedAccountIndexUseCase(userWalletId = userWalletId) + .onRight { derivationIndex -> + uiState.update { + it.updateDerivationIndex(derivationIndex = derivationIndex.value) + } + } + .onLeft { + handleError( + error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, + params = mapOf("userWalletId" to userWalletId.stringValue), + ) + + return@launch + } + } + } + + private fun handleError(error: AccountFeatureError, params: Map = mapOf()) { + val exception = IllegalStateException(error.toString()) + + Timber.e(exception) + + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent(exception = exception, params = params), + ) + + messageSender.showErrorDialog(universalError = error, onDismiss = router::pop) + } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 4b133a4d97..330fc2352f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -1,8 +1,8 @@ package com.tangem.features.account.createedit.entity +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.features.account.common.CryptoPortfolioIconUM import kotlinx.collections.immutable.ImmutableList data class AccountCreateEditUM( @@ -17,11 +17,23 @@ data class AccountCreateEditUM( data class Account( val name: String, val portfolioIcon: CryptoPortfolioIconUM, - val derivationInfo: TextReference, + val derivationInfo: DerivationInfo, val inputPlaceholder: TextReference, val onNameChange: (String) -> Unit, ) + sealed interface DerivationInfo { + val text: TextReference + val index: Int? + + data class Content(override val text: TextReference, override val index: Int) : DerivationInfo + + data object Empty : DerivationInfo { + override val text: TextReference = TextReference.EMPTY + override val index: Int? = null + } + } + data class Colors( val selected: CryptoPortfolioIcon.Color, val list: ImmutableList, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index 41f12322bb..24b69681c9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -1,17 +1,17 @@ package com.tangem.features.account.createedit.entity +import com.tangem.common.ui.account.toUM import com.tangem.core.res.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent -import com.tangem.features.account.common.toUM import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -internal class AccountCreateEditUMBuilder @Inject constructor( - val params: AccountCreateEditComponent.Params, +internal class AccountCreateEditUMBuilder( + private val params: AccountCreateEditComponent.Params, ) { private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList() @@ -29,14 +29,16 @@ internal class AccountCreateEditUMBuilder @Inject constructor( is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account( name = "", portfolioIcon = createIcon, - derivationInfo = TextReference.EMPTY, + derivationInfo = AccountCreateEditUM.DerivationInfo.Empty, inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account), onNameChange = onNameChange, ) is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( - name = params.account.name.value, + name = params.account.accountName.value, portfolioIcon = params.account.portfolioIcon.toUM(), - derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ? + derivationInfo = createAccountDerivationInfo( + index = (params.account as Account.CryptoPortfolio).derivationIndex.value, + ), inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), onNameChange = onNameChange, ) @@ -113,5 +115,25 @@ internal class AccountCreateEditUMBuilder @Inject constructor( fun AccountCreateEditUM.updateButton(isButtonEnabled: Boolean): AccountCreateEditUM { return this.copy(buttonState = this.buttonState.copy(isButtonEnabled = isButtonEnabled)) } + + fun AccountCreateEditUM.updateDerivationIndex(derivationIndex: Int): AccountCreateEditUM { + return this.copy( + account = this.account.copy( + derivationInfo = createAccountDerivationInfo(index = derivationIndex), + ), + ) + } + + private fun createAccountDerivationInfo(index: Int): AccountCreateEditUM.DerivationInfo { + val derivationIndexText = if (index.toString().length == 1) "0$index" else "$index" + + return AccountCreateEditUM.DerivationInfo.Content( + text = resourceReference( + id = R.string.account_form_account_index, + formatArgs = wrappedList(derivationIndexText), + ), + index = index, + ) + } } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt new file mode 100644 index 0000000000..9ab8549fc7 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt @@ -0,0 +1,30 @@ +package com.tangem.features.account.createedit.error + +import com.tangem.core.error.UniversalError + +sealed interface AccountFeatureError : UniversalError { + + val subsystemCode: String + val specificErrorCode: String + + override val errorCode: Int + get() = "108$subsystemCode$specificErrorCode".toInt() + + sealed interface CreateAccount : AccountFeatureError { + + override val subsystemCode: String get() = "001" + + data object UnableToGetDerivationIndex : CreateAccount { + override val specificErrorCode: String = "001" + } + } + + sealed interface EditAccount : AccountFeatureError { + + override val subsystemCode: String get() = "002" + + data object RequiredCryptoPortfolio : EditAccount { + override val specificErrorCode: String = "001" + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 2f98a553bc..639b44419e 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -24,6 +24,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.R +import com.tangem.common.ui.account.AccountIcon +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountIconSize import com.tangem.common.ui.account.getResId import com.tangem.common.ui.account.getUiColor import com.tangem.core.ui.components.PrimaryButton @@ -32,14 +35,10 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.fields.AutoSizeTextField -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.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.features.account.common.toUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account import kotlinx.collections.immutable.toImmutableList @@ -72,11 +71,11 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi SpacerH24() AccountColor(state.colorsState) SpacerH24() - AccountIcon(state.iconsState) + AccountIcons(state.iconsState) SpacerH8() Text( modifier = Modifier.padding(horizontal = 8.dp), - text = state.account.derivationInfo.resolveReference(), + text = state.account.derivationInfo.text.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -93,7 +92,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi } @Composable -private fun AccountSummary(account: AccountCreateEditUM.Account) { +private fun AccountSummary(account: Account) { Column( modifier = Modifier .clip(RoundedCornerShape(16.dp)) @@ -103,7 +102,11 @@ private fun AccountSummary(account: AccountCreateEditUM.Account) { ) { Spacer(modifier = Modifier.height(24.dp)) - AccountIcon(account) + AccountIcon( + name = stringReference(account.name), + icon = account.portfolioIcon, + size = AccountIconSize.Large, + ) Spacer(modifier = Modifier.height(24.dp)) Text( @@ -125,34 +128,6 @@ private fun AccountSummary(account: AccountCreateEditUM.Account) { } } -@Composable -private fun AccountIcon(account: AccountCreateEditUM.Account) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(88.dp) - .clip(RoundedCornerShape(TangemTheme.dimens.radius24)) - .background(account.portfolioIcon.color.getUiColor()), - ) { - val icon = account.portfolioIcon.value - val letter = account.name.firstOrNull() - ?: account.inputPlaceholder.resolveReference().first() - when { - icon == CryptoPortfolioIcon.Icon.Letter -> Text( - text = letter.uppercase(), - style = TangemTheme.typography.head, - color = TangemTheme.colors.text.constantWhite, - ) - else -> Icon( - modifier = Modifier.size(44.dp), - tint = TangemTheme.colors.text.constantWhite, - imageVector = ImageVector.vectorResource(id = icon.getResId()), - contentDescription = null, - ) - } - } -} - @Suppress("LongMethod", "MagicNumber") @Composable private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { @@ -204,7 +179,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { @Suppress("LongMethod", "MagicNumber") @Composable -private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) { +private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) { Box( Modifier .clip(RoundedCornerShape(16.dp)) @@ -299,7 +274,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider Text( - text = letter.uppercase(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.constantWhite, - ) - else -> Icon( - modifier = Modifier.size(20.dp), - tint = TangemTheme.colors.text.constantWhite, - imageVector = ImageVector.vectorResource(id = icon.getResId()), - contentDescription = null, - ) - } - } -} - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -217,20 +170,18 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider:: private class PreviewStateProvider : CollectionPreviewParameterProvider( buildList { - var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + val accountName = "Main" + var portfolioIcon = AccountIconPreviewData.randomAccountIcon() val first = AccountDetailsUM( onCloseClick = {}, onAccountEditClick = {}, onManageTokensClick = {}, onArchiveAccountClick = {}, - accountName = "Main", + accountName = accountName, accountIcon = portfolioIcon, ) add(first) - portfolioIcon = portfolioIcon.copy( - value = CryptoPortfolioIcon.Icon.Letter, - color = CryptoPortfolioIcon.Color.entries.random(), - ) + portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true) add(first.copy(accountIcon = portfolioIcon)) }, ) \ No newline at end of file diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index 4ee4b4358b..def97bf1f3 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { api(projects.features.biometry.api) + implementation(projects.features.hotWallet.api) /** Core modules */ implementation(projects.core.ui) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 933716b04b..614b462e24 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -13,13 +13,15 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -40,11 +42,13 @@ internal class AskBiometryModel @Inject constructor( private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, private val settingsRepository: SettingsRepository, private val tangemSdkManager: TangemSdkManager, - private val userWalletsListManager: UserWalletsListManager, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val walletsRepository: WalletsRepository, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsManager: SettingsManager, private val uiMessageSender: UiMessageSender, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -87,7 +91,7 @@ internal class AskBiometryModel @Inject constructor( * because it will be automatically saved on UserWalletsListManager switch */ - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync ?: run { + val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run { Timber.e("Unable to save user wallet") uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), @@ -109,10 +113,18 @@ internal class AskBiometryModel @Inject constructor( walletsRepository.saveShouldSaveUserWallets(item = true) settingsRepository.setShouldSaveAccessCodes(value = true) - if (userWallet is UserWallet.Cold) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + walletsRepository.setUseBiometricAuthentication(value = true) + setBiometryLockForAllWallets() cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, + isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(), ) + } else { + if (userWallet is UserWallet.Cold) { + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) + } } if (_uiState.value.bottomSheetVariant) { @@ -123,6 +135,18 @@ internal class AskBiometryModel @Inject constructor( params.modelCallbacks.onAllowed() } + private fun setBiometryLockForAllWallets() { + modelScope.launch { + userWalletsListRepository.userWalletsSync().forEach { userWallet -> + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + } + private fun showEnrollBiometricsDialog() { uiMessageSender.send( DialogMessage( diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 477a6af3ea..da58579502 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -18,6 +18,12 @@ dependencies { /** Hot Wallet Feature */ implementation(projects.features.hotWallet.api) + /** Project - Domain */ + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.wallets) + implementation(projects.domain.models) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.analytics) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 242046b568..23d94a6c78 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -1,19 +1,63 @@ package com.tangem.features.createwalletselection +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -31,10 +75,110 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onHardwareWalletClick() { - // TODO open card order web page + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onScanClick() { - // TODO open card scanning + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) } } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 0c777f9a97..4106c3d8c6 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,10 +5,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -18,6 +20,7 @@ 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.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -179,6 +182,9 @@ private fun AlreadyHaveTangemWalletBlock( isScanInProgress: Boolean, modifier: Modifier = Modifier, ) { + var buttonWidth by remember { mutableStateOf(0) } + val density = LocalDensity.current + Row( modifier = modifier .fillMaxWidth() @@ -201,9 +207,17 @@ private fun AlreadyHaveTangemWalletBlock( style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, ) + TangemButton( modifier = Modifier - .wrapContentWidth(), + .conditional(buttonWidth > 0) { + width(with(density) { buttonWidth.toDp() }) + } + .onGloballyPositioned { coordinates -> + if (buttonWidth == 0) { + buttonWidth = coordinates.size.width + } + }, text = stringResourceSafe(R.string.wallet_create_scan_title), onClick = onScanClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index e44ec003ea..8f41e0f6c1 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) + implementation(projects.features.createWalletSelection.api) + implementation(projects.features.hotWallet.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..f217f837ce 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -11,4 +12,5 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 6af5b54fe3..bfdc9b284b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -6,12 +6,19 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R.* +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -20,22 +27,28 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class UserWalletListModel @Inject constructor( userWalletsFetcherFactory: UserWalletsFetcher.Factory, shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val router: Router, private val messageSender: UiMessageSender, - private val userWalletSaver: UserWalletSaver, override val dispatchers: CoroutineDispatcherProvider, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletSaver: UserWalletSaver, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = false, + authMode = false, onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, ) @@ -44,7 +57,8 @@ internal class UserWalletListModel @Inject constructor( userWallets = persistentListOf(), isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, - onAddNewWalletClick = ::addUserWallet, + onAddNewWalletClick = ::onAddNewWalletClick, + addWalletBottomSheet = TangemBottomSheetConfig.Empty, ), ) @@ -53,8 +67,9 @@ internal class UserWalletListModel @Inject constructor( flow = userWalletsFetcher.userWallets, flow2 = shouldSaveUserWalletsUseCase(), flow3 = isWalletSavingInProgress, - transform = ::updateState, - ).launchIn(modelScope) + ) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress -> + updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress) + }.launchIn(modelScope) } private fun updateState( @@ -65,7 +80,7 @@ internal class UserWalletListModel @Inject constructor( value.copy( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, - addNewWalletText = if (shouldSaveUserWallets) { + addNewWalletText = if (shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled) { resourceReference(R.string.user_wallet_list_add_button) } else { resourceReference(R.string.scan_card_settings_button) @@ -73,7 +88,64 @@ internal class UserWalletListModel @Inject constructor( ) } - private fun addUserWallet() = withProgress(isWalletSavingInProgress) { - userWalletSaver.scanAndSaveUserWallet(modelScope) + private fun onAddNewWalletClick() { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismissAddWalletBottomSheet, + content = createAddWalletBottomSheetContent(), + ), + ) + } + } else { + withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet(modelScope) + } + } + } + + private fun dismissAddWalletBottomSheet() { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false), + ) + } + } + + private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent { + return OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + dismissAddWalletBottomSheet() + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + }, + ) + } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index ec27900868..bea3f8c35d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -15,9 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent @@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M onClick = state.onAddNewWalletClick, ) } + + AddWalletBottomSheet(state.addWalletBottomSheet) } @Composable @@ -94,6 +100,15 @@ private fun AddWalletButton( } } +@Composable +private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index a1d162c8a3..50fe65280b 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -53,7 +53,7 @@ internal class DisclaimerModel @Inject constructor( val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() if (shouldAskPushPermission && !isHuaweiDevice) { - router.push(AppRoute.PushNotification) + router.push(AppRoute.PushNotification(AppRoute.PushNotification.Source.Stories)) } else { neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION) diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 4a4f17d3bf..d06bedde20 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -51,9 +51,6 @@ dependencies { implementation(deps.compose.coil) implementation(deps.decompose.ext.compose) - /** Firebase */ - implementation(deps.firebase.analytics) - /** Tangem libraries */ implementation(tangemDeps.card.android) implementation(tangemDeps.card.core) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt deleted file mode 100644 index 16c104323b..0000000000 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.features.home.impl.analytics - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.card.CardTypesResolver -import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam - -internal class ParamCardCurrencyConverter : Converter { - - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency - - val type = when { - value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) - value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin) - value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) - value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!) - else -> null - } ?: return null - - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) - } -} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 61614e3a62..70636eda60 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -1,7 +1,5 @@ package com.tangem.features.home.impl.model -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute @@ -23,20 +21,22 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.home.api.HomeComponent -import com.tangem.features.home.impl.analytics.IntroductionProcess -import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter -import com.tangem.features.home.impl.analytics.Shop import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.getRestrictedStories @@ -64,16 +64,17 @@ internal class HomeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, - private val saveWalletUseCase: SaveWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, - private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val router: Router, - private val selectWalletUseCase: SelectWalletUseCase, private val appRouter: AppRouter, private val getUserCountryUseCase: GetUserCountryUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { @@ -135,10 +136,9 @@ internal class HomeModel @Inject constructor( private fun onShopClick() { analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) analyticsEventHandler.send(Shop.ScreenOpened) - - Firebase.analytics.appInstanceId - .addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") } - .addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) } + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onSearchTokensClick() { @@ -198,24 +198,17 @@ internal class HomeModel @Inject constructor( saveWalletUseCase(userWallet).fold( ifLeft = { - Timber.e(it.toString(), "Unable to save user wallet") + delay(HIDE_PROGRESS_DELAY) setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } }, ifRight = { + setLoading(false) sendSignedInCardAnalyticsEvent(scanResponse) - - // Select the wallet using new mechanism - selectWalletUseCase(userWallet.walletId).fold( - ifLeft = { - Timber.e("Unable to select user wallet: $it") - setLoading(false) - }, - ifRight = { - delay(HIDE_PROGRESS_DELAY) - setLoading(false) - appRouter.replaceAll(AppRoute.Wallet) - }, - ) + appRouter.replaceAll(AppRoute.Wallet) }, ) } @@ -228,7 +221,7 @@ internal class HomeModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = "1", + walletsCount = userWalletsListManager.walletsCount.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) @@ -241,15 +234,9 @@ internal class HomeModel @Inject constructor( fun handleScanError(error: TangemError) { when (error) { - is TangemSdkError.NfcFeatureIsUnavailable -> { - handleNfcFeatureUnavailable() - } - is TangemSdkError -> { - Timber.e(error, "Scan error occurred") - } - else -> { - Timber.e(error, "Error happened") - } + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") } } @@ -261,8 +248,4 @@ internal class HomeModel @Inject constructor( ), ) } - - companion object { - const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" - } } \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt new file mode 100644 index 0000000000..84d8828d8f --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface CreateWalletBackupComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index ccc57778cd..2f25fc3478 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.datasource) /** Domain */ + implementation(projects.domain.card) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index a7e9eec930..2db69f48ee 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -5,10 +5,11 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth @@ -27,7 +28,8 @@ internal class AccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, - private val saveWalletUseCase: SaveWalletUseCase, + private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, private val tangemHotSdk: TangemHotSdk, ) : Model() { @@ -77,15 +79,45 @@ internal class AccessCodeModel @Inject constructor( runCatching { val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet with id $userWalletId not found") } - if (userWallet is UserWallet.Hot) { - val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) - val updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, - auth = HotAuth.Password(accessCode.toCharArray()), + if (userWallet !is UserWallet.Hot) return@launch + + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = updatedHotWalletId, + auth = HotAuth.Password(accessCode.toCharArray()), + ), + auth = HotAuth.Biometry, ) - saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId), canOverride = true) - params.callbacks.onAccessCodeConfirmed(params.userWalletId) } + + userWalletsListRepository.saveWithoutLock( + userWallet.copy( + hotWalletId = updatedHotWalletId, + backedUp = true, + ), + canOverride = true, + ) + + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), + ) + + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } + + params.callbacks.onAccessCodeConfirmed(params.userWalletId) }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 987c94c953..3924d978c7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.res.R import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -73,8 +74,9 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { ) { PinTextField( length = state.accessCodeLength, - isPasswordVisual = true, + isPasswordVisual = !state.isConfirmMode, value = state.accessCode, + pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index 5bacfc9fef..4dd7d01895 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -26,12 +26,13 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( } override suspend fun successfulAuthentication() { - // TODO handle successful authentication - // TODO add delay + model.successfulAuthentication() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { - model.show(hasBiometry) + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result { + model.show(attemptRequest) return model.waitResult() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 305bd305bb..4a75ad8438 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -2,37 +2,64 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM +import com.tangem.features.hotwallet.impl.R import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { private val result = MutableStateFlow(null) + private val currentRequest = MutableStateFlow(null) + private val attemptsRequestJobHolder = JobHolder() + + private val HotWalletPasswordRequester.AttemptRequest.attemptId + get() = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = authMode, + ) val uiState: StateFlow field = MutableStateFlow(getInitialState()) - fun dismiss() { - result.value = HotWalletPasswordRequester.Result.Dismiss - dismissState() - } + suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) { + if (userWalletExists(attemptRequest.hotWalletId).not()) { + Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") + result.value = HotWalletPasswordRequester.Result.Dismiss + return + } - fun show(hasBiometry: Boolean) { + currentRequest.value = attemptRequest result.value = null // Reset the result when showing the dialog + subscribeToAttempts(id = attemptRequest.attemptId) uiState.update { it.copy( isShown = true, accessCode = "", - useBiometricVisible = hasBiometry, + useBiometricVisible = attemptRequest.hasBiometry, onAccessCodeChange = ::onAccessCodeChange, ) } @@ -42,16 +69,36 @@ internal class HotAccessCodeRequestModel @Inject constructor( return result.filterNotNull().first().also { result.value = null } } + fun dismiss() { + result.value = HotWalletPasswordRequester.Result.Dismiss + attemptsRequestJobHolder.cancel() + dismissState() + } + suspend fun wrongAccessCode() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.incrementAttempts(currentRequest.attemptId) uiState.update { it.copy( - wrongAccessCode = true, + accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, ) } delay(timeMillis = 500) // Delay to show the wrong access code state } + suspend fun successfulAuthentication() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.resetAttempts(currentRequest.hotWalletId) + uiState.update { + it.copy( + accessCodeColor = PinTextColor.Success, + onAccessCodeChange = {}, + ) + } + delay(timeMillis = 200) // Delay to show the success state + } + private fun getInitialState() = HotAccessCodeRequestUM( onDismiss = ::dismiss, onAccessCodeChange = ::onAccessCodeChange, @@ -66,7 +113,10 @@ internal class HotAccessCodeRequestModel @Inject constructor( if (accessCode.length > ACCESS_CODE_LENGTH) return uiState.update { - it.copy(accessCode = accessCode, wrongAccessCode = false) + it.copy( + accessCode = accessCode, + accessCodeColor = PinTextColor.Primary, + ) } if (accessCode.length == ACCESS_CODE_LENGTH) { @@ -78,6 +128,68 @@ internal class HotAccessCodeRequestModel @Inject constructor( } } + private fun subscribeToAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + fun remainingSecondsToText(remainingSeconds: Int): TextReference? { + return if (remainingSeconds > 0) { + resourceReference( + R.string.access_code_check_warining_wait, + wrappedList(remainingSeconds), + ) + } else { + null + } + } + + suspend fun collectAttempts(attempts: Attempts) { + when (attempts) { + is Attempts.FastForward -> { + /** ignore */ + } + is Attempts.WithDelay -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + is Attempts.BeforeDeletion -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds) + ?: resourceReference( + R.string.access_code_check_warining_delete, + wrappedList(attempts.remainingAttemptsCountBeforeDeletion), + ), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + Attempts.Deletion -> deleteUserWallet() + } + } + + modelScope.launch { + hotAccessCodeAttemptsRepository.getAttempts(id) + .collectLatest { attempts -> collectAttempts(attempts) } + }.saveIn(attemptsRequestJobHolder) + } + + private suspend fun userWalletExists(id: HotWalletId): Boolean { + return userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot && it.hotWalletId == id } + } + + private suspend fun deleteUserWallet() { + val currentRequest = currentRequest.value ?: return + val userWallet = userWalletsListRepository.userWalletsSync() + .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return + userWalletsListRepository.delete(listOf(userWallet.walletId)) + dismiss() + } + private fun dismissState() { uiState.update { it.copy(isShown = false) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt index 82b7e51ec8..78c3c269f6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -1,9 +1,13 @@ package com.tangem.features.hotwallet.accesscoderequest.entity +import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference + internal data class HotAccessCodeRequestUM( val isShown: Boolean = false, val accessCode: String = "", - val wrongAccessCode: Boolean = false, + val accessCodeColor: PinTextColor = PinTextColor.Primary, + val wrongAccessCodeText: TextReference? = null, val useBiometricVisible: Boolean = true, val useBiometricClick: () -> Unit = {}, val onAccessCodeChange: (String) -> Unit = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt index b7e7218a48..1968bae87c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -13,20 +13,15 @@ class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordR val componentRequester = MutableStateFlow(null) - override suspend fun wrongPassword() { - call { wrongPassword() } - } + override suspend fun wrongPassword() = call { wrongPassword() } - override suspend fun successfulAuthentication() { - call { successfulAuthentication() } - } + override suspend fun successfulAuthentication() = call { successfulAuthentication() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result = - call { requestPassword(hasBiometry) } + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result = call { requestPassword(attemptRequest) } - override suspend fun dismiss() { - call { dismiss() } - } + override suspend fun dismiss() = call { dismiss() } private suspend fun call(block: suspend HotWalletPasswordRequester.() -> T): T { return withTimeout(timeMillis = 1000) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index bc64b660c4..2e3b61e43d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -13,6 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButton @@ -20,7 +22,10 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager @@ -85,9 +90,36 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM length = 6, isPasswordVisual = true, value = state.accessCode, - wrongCode = state.wrongAccessCode, + pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) + + SpacerH(20.dp) + + AnimatedVisibility( + modifier = Modifier.animateEnterExit( + enter = slideInVertically( + tween(), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween()), + exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), + ), + visible = state.wrongAccessCodeText != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + val wrongAccessCodeText = + state.wrongAccessCodeText ?: return@AnimatedVisibility + + Text( + text = wrongAccessCodeText.resolveReference(), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.warning, + ) + } } if (state.useBiometricVisible) { @@ -97,7 +129,10 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM .fillMaxWidth() .navigationBarsPadding() .imePadding(), - text = "Use biometric", + text = stringResourceSafe( + id = R.string.welcome_unlock, + stringResourceSafe(R.string.common_biometrics), + ), onClick = state.useBiometricClick, ) } @@ -106,9 +141,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM val hapticManager = LocalHapticManager.current - LaunchedEffect(state.wrongAccessCode) { - if (state.wrongAccessCode) { - hapticManager.perform(TangemHapticEffect.View.Reject) + LaunchedEffect(state.accessCodeColor) { + when (state.accessCodeColor) { + PinTextColor.WrongCode -> { + hapticManager.perform(TangemHapticEffect.View.Reject) + } + PinTextColor.Success -> { + hapticManager.perform(TangemHapticEffect.View.Confirm) + } + else -> Unit } } } @@ -125,7 +166,10 @@ private fun Preview() { var isShown by remember { mutableStateOf(true) } HotAccessCodeRequestFullScreenContent( - state = HotAccessCodeRequestUM(isShown = isShown), + state = HotAccessCodeRequestUM( + isShown = isShown, + wrongAccessCodeText = stringReference("Wrong access code"), + ), modifier = Modifier, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index 1e0fd88f21..de7e7a76f2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -1,5 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.entry.routing +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel @@ -63,6 +64,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( context = childContext, params = PushNotificationsParams( modelCallbacks = model.pushNotificationsCallbacks, + source = AppRoute.PushNotification.Source.Onboarding, ), ) is AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt index 667d538a14..efd6c04702 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.entity import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -18,6 +17,5 @@ internal data class AddExistingWalletImportUM( val importWalletClick: () -> Unit, val suggestionsList: ImmutableList, val onSuggestionClick: (String) -> Unit, - val infoBottomSheetConfig: TangemBottomSheetConfig, val readyToImport: Boolean, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index f11d60b7bc..60f3305500 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,9 +1,21 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.MnemonicRepository @@ -19,6 +31,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletImportModel @Inject constructor( paramsContainer: ParamsContainer, @@ -27,12 +40,29 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder + private val passphraseInfoAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_56) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.common_passphrase) + body = resourceReference(R.string.onboarding_bottom_sheet_passphrase_description) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + init { importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, @@ -45,6 +75,7 @@ internal class AddExistingWalletImportModel @Inject constructor( passphrase = passphrase, ) }, + onPassphraseInfoClick = ::onPassphraseInfoClick, ) } @@ -54,23 +85,42 @@ internal class AddExistingWalletImportModel @Inject constructor( @Suppress("UnusedPrivateMember") private fun importWallet(mnemonic: Mnemonic, passphrase: String?) { modelScope.launch { - uiState.update { - it.copy(importWalletProgress = true) - } + setImportProgress(true) runCatching { val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() - saveUserWalletUseCase(userWallet) - params.callbacks.onWalletImported(userWallet.walletId) + saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true)) + .onLeft { + setImportProgress(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> { + uiMessageSender.send( + SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), + ) + } + } + } + .onRight { + setImportProgress(false) + params.callbacks.onWalletImported(userWallet.walletId) + } }.onFailure { Timber.e(it) - - uiState.update { - it.copy(importWalletProgress = false) - } + setImportProgress(false) } } } + + private fun setImportProgress(progress: Boolean) { + uiState.update { + it.copy(importWalletProgress = progress) + } + } + + private fun onPassphraseInfoClick() { + uiMessageSender.send(passphraseInfoAlertBS) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index b09bdd3265..7f89025442 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult @@ -24,6 +23,7 @@ internal class ImportSeedPhraseUiStateBuilder( private val readyToImport: (Boolean) -> Unit, private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit, private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit, + private val onPassphraseInfoClick: () -> Unit, ) { private val wordsCheckJobHolder = JobHolder() private var importedMnemonic: Mnemonic? = null @@ -49,11 +49,10 @@ internal class ImportSeedPhraseUiStateBuilder( passphrase = it.text updateUiState { state -> state.copy(passPhrase = it) } }, - onPassphraseInfoClick = ::showInfoBS, + onPassphraseInfoClick = onPassphraseInfoClick, importWalletClick = ::onCreateWallet, onSuggestionClick = { word -> addSuggestedWord(word) }, readyToImport = false, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, ) } @@ -168,19 +167,6 @@ internal class ImportSeedPhraseUiStateBuilder( } } - private fun showInfoBS() { - updateUiState { state -> - state.copy( - infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty.copy( - isShown = true, - onDismissRequest = { - updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) } - }, - ), - ) - } - } - companion object { private const val MINIMUM_WORD_LENGTH = 2 private const val WORDS_INTERCEPT_DELAY_MS = 500L diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index 9d1d73601c..d7bb9338b0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -37,7 +37,6 @@ import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.OutlineTextFieldWithIcon import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation @@ -113,8 +112,6 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) } } - - PassphraseInfoBottomSheet(state.infoBottomSheetConfig) } @Composable @@ -233,7 +230,6 @@ private fun PreviewAddExistingWalletImportContent() { importWalletClick = {}, suggestionsList = persistentListOf(), onSuggestionClick = {}, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, readyToImport = false, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt deleted file mode 100644 index 76521aab7a..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.hotwallet.addexistingwallet.im.port.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.primary, - ) { _: TangemBottomSheetConfigContent.Empty -> - PassphraseInfoBottomSheetContent(config.onDismissRequest) - } -} - -@Composable -fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - ) { - Icon( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = TangemTheme.dimens.size40) - .size(TangemTheme.dimens.size48), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - - Text( - text = stringResourceSafe(id = R.string.common_passphrase), - modifier = Modifier - .padding(top = TangemTheme.dimens.size40) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - - Text( - text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), - modifier = Modifier - .padding(top = TangemTheme.dimens.size16) - .padding(horizontal = TangemTheme.dimens.size24) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - ) - - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.size16) - .padding(top = TangemTheme.dimens.size40) - .padding(bottom = TangemTheme.dimens.size32) - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_ok), - onClick = onDismiss, - ) - } -} - -@Preview -@Composable -private fun PassphraseInfoBottomSheetContentPreview() { - TangemThemePreview { - PassphraseInfoBottomSheetContent({ }) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt index 58ea450355..dab15c9946 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -1,18 +1,63 @@ package com.tangem.features.hotwallet.addexistingwallet.start +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletStartModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val saveWalletUseCase: SaveWalletUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletStartComponent.Params = paramsContainer.require() @@ -20,10 +65,119 @@ internal class AddExistingWalletStartModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow( AddExistingWalletStartUM( + isScanInProgress = false, onBackClick = params.callbacks::onBackClick, onImportPhraseClick = params.callbacks::onImportPhraseClick, - onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ }, - onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ }, + onScanCardClick = ::onScanClick, + onBuyCardClick = ::onShopClick, ), ) + + private fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt index f898f9c1b7..37a5113f35 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.entity internal data class AddExistingWalletStartUM( + val isScanInProgress: Boolean, val onBackClick: () -> Unit, val onImportPhraseClick: () -> Unit, val onScanCardClick: () -> Unit, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index eef14e30c9..6bf693e36b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -74,14 +75,25 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi title = stringResourceSafe(R.string.wallet_import_scan_title), description = stringResourceSafe(R.string.wallet_import_scan_description), badge = { - Icon( - modifier = Modifier - .padding(top = 2.dp) - .size(20.dp), - painter = painterResource(R.drawable.ic_tangem_24), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) + if (state.isScanInProgress) { + CircularProgressIndicator( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp) + .padding(2.dp), + color = TangemTheme.colors.text.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } else { + Icon( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp), + painter = painterResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + } }, onClick = state.onScanCardClick, enabled = true, @@ -160,6 +172,7 @@ private fun PreviewCreateWalletContent() { TangemThemePreview { AddExistingWalletStartContent( state = AddExistingWalletStartUM( + isScanInProgress = true, onBackClick = {}, onImportPhraseClick = {}, onScanCardClick = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index d59cf2e39f..0b3261b4f3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -45,9 +45,8 @@ internal class CreateMobileWalletModel @Inject constructor( runCatching { val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) - saveUserWalletUseCase( - hotUserWalletBuilder.build(), - ) + val userWallet = hotUserWalletBuilder.build() + saveUserWalletUseCase(userWallet) router.replaceAll(AppRoute.Wallet) }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt new file mode 100644 index 0000000000..8cc687e476 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt @@ -0,0 +1,97 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ModelScoped +internal class CreateWalletBackupModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + val params = paramsContainer.require() + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks() + val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks() + val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks() + val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onBack() { + when (currentRoute.value) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop() + is CreateWalletBackupRoute.RecoveryPhrase -> stackNavigation.pop() + is CreateWalletBackupRoute.ConfirmBackup -> stackNavigation.pop() + is CreateWalletBackupRoute.BackupCompleted -> router.pop() + } + } + + fun onManualBackupStarted() { + stackNavigation.push(CreateWalletBackupRoute.RecoveryPhrase) + } + + fun onManualBackupPhraseShown() { + stackNavigation.push(CreateWalletBackupRoute.ConfirmBackup) + } + + fun onManualBackupChecked() { + stackNavigation.push(CreateWalletBackupRoute.BackupCompleted) + } + + fun onManualBackupCompleted() { + router.pop() + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onBack() + } + + override fun onSkipClick() = Unit + } + + inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupStarted() + } + } + + inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupPhraseShown() + } + } + + inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks { + override fun onCompleteClick() { + onManualBackupChecked() + } + } + + inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + onManualBackupCompleted() + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt new file mode 100644 index 0000000000..5b8c3a2e68 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt @@ -0,0 +1,56 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import javax.inject.Inject + +internal class CreateWalletBackupStepperStateManager @Inject constructor() { + + fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_START, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_PHRASE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_CONFIRM, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_COMPLETED, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 4 + + private const val STEP_START = 1 + private const val STEP_PHRASE = 2 + private const val STEP_CONFIRM = 3 + private const val STEP_COMPLETED = 4 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt new file mode 100644 index 0000000000..b13c9193f9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory +import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultCreateWalletBackupComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: CreateWalletBackupComponent.Params, + private val stepperStateManager: CreateWalletBackupStepperStateManager, + createWalletBackupChildFactory: CreateWalletBackupChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, +) : CreateWalletBackupComponent, AppComponentContext by appComponentContext { + + private val model: CreateWalletBackupModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "createWalletBackupInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createWalletBackupChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + + CreateWalletBackupContent( + stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : CreateWalletBackupComponent.Factory { + override fun create( + context: AppComponentContext, + params: CreateWalletBackupComponent.Params, + ): DefaultCreateWalletBackupComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt new file mode 100644 index 0000000000..5ea687703d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager +import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateWalletBackupModuleBinds { + + @Binds + @Singleton + fun bindCreateWalletBackupComponentFactory( + impl: DefaultCreateWalletBackupComponent.Factory, + ): CreateWalletBackupComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateWalletBackupModel::class) + fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateWalletBackupModule { + + @Provides + @Singleton + fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager { + return CreateWalletBackupStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt new file mode 100644 index 0000000000..44d740af6c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import javax.inject.Inject + +internal class CreateWalletBackupChildFactory @Inject constructor() { + + fun createChild( + route: CreateWalletBackupRoute, + childContext: AppComponentContext, + model: CreateWalletBackupModel, + ): ComposableContentComponent = when (route) { + CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent( + context = childContext, + params = ManualBackupStartComponent.Params( + callbacks = model.manualBackupStartModelCallbacks, + ), + ) + CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent( + context = childContext, + params = ManualBackupPhraseComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupPhraseModelCallbacks, + ), + ) + CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent( + context = childContext, + params = ManualBackupCheckComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCheckModelCallbacks, + ), + ) + CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent( + context = childContext, + params = ManualBackupCompletedComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCompletedModelCallbacks, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt new file mode 100644 index 0000000000..f063a1f796 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt @@ -0,0 +1,19 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface CreateWalletBackupRoute { + + @Serializable + data object RecoveryPhraseStart : CreateWalletBackupRoute + + @Serializable + data object RecoveryPhrase : CreateWalletBackupRoute + + @Serializable + data object ConfirmBackup : CreateWalletBackupRoute + + @Serializable + data object BackupCompleted : CreateWalletBackupRoute +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt new file mode 100644 index 0000000000..e6f1e9e732 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +@Composable +internal fun CreateWalletBackupContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt index 59762e8fe3..e6c13bdbd9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt @@ -1,12 +1,13 @@ package com.tangem.features.hotwallet.walletactivation.entry.routing +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent -import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -72,6 +73,7 @@ internal class WalletActivationChildFactory @Inject constructor( context = childContext, params = PushNotificationsParams( modelCallbacks = model.pushNotificationsCallbacks, + source = AppRoute.PushNotification.Source.Onboarding, ), ) is WalletActivationRoute.SetupFinished -> MobileWalletSetupFinishedComponent( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index 5df45d62a0..f3f17cd9d3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -8,6 +8,7 @@ internal data class WalletBackupUM( val googleDriveStatus: LabelUM?, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, + val backedUp: Boolean, ) internal sealed class BackupStatus { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 18e601c406..b398ed047a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -1,15 +1,24 @@ package com.tangem.features.hotwallet.walletbackup.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.common.routing.AppRoute import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -22,6 +31,7 @@ internal class WalletBackupModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() @@ -38,11 +48,31 @@ internal class WalletBackupModel @Inject constructor( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), - onRecoveryPhraseClick = { }, + onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, + backedUp = false, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + closeBs() + } + } + } + init { getWalletUseCase.invokeFlow(params.userWalletId) .map { it.getOrNull() } @@ -80,5 +110,14 @@ internal class WalletBackupModel @Inject constructor( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + backedUp = userWallet.backedUp, ) + + private fun onRecoveryPhraseClick() { + if (uiState.value.backedUp) { + // TODO [REDACTED_TASK_KEY] + } else { + uiMessageSender.send(makeBackupAtFirstAlertBS) + } + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 66e0c92d43..f5d25b7280 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -96,6 +96,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider + val tokenExpirationHandler = object : TokenExpirationHandler { + override fun onTokenExpired() = "" + } + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) + .withTheme(TangemSNSTheme.theme(activity)) + .withIconHandler(TangemSNSIconHandler()) + .withLocale(Locale("en")) + .build() + snsSdk.launch() } } - - val snsSdk = SNSMobileSDK.Builder(activity) - .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) - .withConf(SNSInitConfig(strings = mapOf())) - .withTheme(TangemSNSTheme.theme(activity)) - .withIconHandler(TangemSNSIconHandler()) - .withLocale(Locale("en")) - .withCompleteHandler( - object : SNSCompleteHandler { - override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) { - } - }, - ) - .build() - - snsSdk.launch() } + model.getKycToken(params) } @AssistedFactory diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt new file mode 100644 index 0000000000..aff81ad334 --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -0,0 +1,32 @@ +package com.tangem.features.kyc + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +class DefaultKycModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + kycRepositoryFactory: KycRepository.Factory, +) : Model() { + + private val kycRepository = kycRepositoryFactory.create() + + private val _uiState: MutableStateFlow = MutableStateFlow(null) + val uiState = _uiState.asStateFlow() + + fun getKycToken(params: KycComponent.Params) { + modelScope.launch { + kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull() + ?.let { _uiState.emit(it) } + } + } +} \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt index c72f282775..a2fefafc7b 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt @@ -1,11 +1,16 @@ package com.tangem.features.kyc.di +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model import com.tangem.features.kyc.DefaultKycComponent +import com.tangem.features.kyc.DefaultKycModel import com.tangem.features.kyc.KycComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module @InstallIn(SingletonComponent::class) @@ -13,4 +18,13 @@ internal interface FeatureModule { @Binds fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(DefaultKycModel::class) + fun provideModel(model: DefaultKycModel): Model } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index b486703bd8..7664a6ce6e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -93,6 +93,7 @@ internal class ManageTokensListManager @AssistedInject constructor( actionsFlow = actionsFlow, coroutineScope = this, ), + // only for onboarding case, change carefully and check repository implementation loadUserTokensFromRemote = userWalletId != null && source == ManageTokensSource.ONBOARDING, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 5e4e7a4d4d..0778b806c6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -18,9 +18,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -164,11 +165,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( type = percentChangeType.toChartType(), xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), yAxisFormatter = { value -> - BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = value, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ) + value.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + } }, ) } @@ -196,11 +198,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( val state = MutableStateFlow( MarketsTokenDetailsUM( tokenName = params.token.name, - priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = params.token.tokenQuotes.currentPrice, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + priceText = params.token.tokenQuotes.currentPrice.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, dateTimeText = resourceReference(R.string.common_today), priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), @@ -403,7 +406,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( state.update { it.copy( - priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceText = newInfo.quotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + }, priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( interval = it.selectedInterval, ), @@ -490,7 +498,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: getDefaultDateTimeString(currentState.selectedInterval) - val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value) + val priceText = (price ?: currentQuotes.value.currentPrice).format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + } val percent = price?.let { getChangePercentBetween( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt index 0ddafb4e83..eb55abd9dd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt @@ -5,7 +5,9 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.markets.TokenMarketExchange import com.tangem.domain.markets.TokenMarketExchange.TrustScore import com.tangem.features.markets.impl.R @@ -29,11 +31,12 @@ internal object ExchangeItemStateConverter : Converter h24ChangePercent @@ -66,8 +57,8 @@ internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: Bi } internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = currentPrice).first - val updated = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = updatedPrice).first + val current = getFiatPriceAmountWithScale(value = currentPrice).first + val updated = getFiatPriceAmountWithScale(value = updatedPrice).first return when { updated > current -> PriceChangeType.UP diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt index 4bf8b58e27..74d10c5742 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -3,6 +3,9 @@ package com.tangem.features.markets.details.impl.model.state import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo @@ -57,7 +60,12 @@ internal class QuotesStateUpdater( state.update { stateToUpdate -> stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceText = newQuotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency().symbol, + fiatCurrencyCode = currentAppCurrency().code, + ).price() + }, priceChangePercentText = newQuotes.getFormattedPercentByInterval( interval = stateToUpdate.selectedInterval, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 372309d5ea..bb1496cfe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -11,9 +11,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetCurrencyQuotesUseCase @@ -86,13 +87,14 @@ internal class TokenMarketBlockModel @Inject constructor( ) state.value = state.value.copy( - currentPrice = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = res.fiatRate, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencyCode = currentAppCurrency.value.code, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + currentPrice = res.fiatRate.format { + fiat( + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencyCode = currentAppCurrency.value.code, + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index d7352826b5..22712a972c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -7,11 +7,7 @@ import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.impl.R @@ -94,11 +90,12 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotesShort?.currentPrice - val priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = tokenQuotesShort.currentPrice, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } val changeType = if (prevPrice != null) { if (tokenQuotesShort.currentPrice > prevPrice) { diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt index ed1df0a1f2..801c8f6420 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt @@ -2,8 +2,9 @@ package com.tangem.features.nft.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.nft.models.NFTAsset +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.nft.models.NFTAsset interface NFTDetailsBlockComponent : ComposableContentComponent { @@ -11,6 +12,8 @@ interface NFTDetailsBlockComponent : ComposableContentComponent { val userWalletId: UserWalletId, val nftAsset: NFTAsset, val nftCollectionName: String, + val title: TextReference, + val isSuccessScreen: Boolean, ) interface Factory : ComponentFactory diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt index 234cb0adfb..4856ca5634 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt @@ -22,6 +22,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor( assetName = stringReference(params.nftAsset.name.orEmpty()), collectionName = stringReference(params.nftCollectionName), assetImage = params.nftAsset.media?.imageUrl, + title = params.title, + isSuccessScreen = params.isSuccessScreen, networkIconRes = getActiveIconRes(params.nftAsset.network.rawId), ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt index 6336d2dbb2..8d7d1148c9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -19,12 +20,15 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.common.ui.NFTLogo import com.tangem.features.nft.impl.R +@Suppress("LongParameterList") @Composable internal fun NFTDetailsBlock( + title: TextReference, assetName: TextReference, collectionName: TextReference, assetImage: String?, networkIconRes: Int, + isSuccessScreen: Boolean, ) { Column( modifier = Modifier @@ -35,20 +39,21 @@ internal fun NFTDetailsBlock( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = "NFT Asset", + text = title.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - NFTLogo( - assetImage, - networkIconRes, - background = TangemTheme.colors.background.action, - ) - + if (isSuccessScreen) { + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { @@ -63,6 +68,14 @@ internal fun NFTDetailsBlock( color = TangemTheme.colors.text.tertiary, ) } + if (!isSuccessScreen) { + SpacerWMax() + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } } } } @@ -78,6 +91,8 @@ private fun NFTDetailsBlock_Preview() { collectionName = stringReference("NFT Collection"), assetImage = null, networkIconRes = R.drawable.img_polygon_22, + title = stringReference("From My Wallet"), + isSuccessScreen = false, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index 6c8cb1fc1b..d4c055fcda 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -73,6 +75,7 @@ internal fun MultiWalletAccessCodeEnter( label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), isError = state.codesNotMatchError, visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 23b2dbb674..0c7b8d6290 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent @@ -51,6 +52,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, private val walletsRepository: WalletsRepository, @@ -231,7 +233,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( OnboardingMultiWalletComponent.Mode.Onboarding, OnboardingMultiWalletComponent.Mode.ContinueFinalize, -> { - userWalletsListManager.save( + saveWalletUseCase( userWallet = userWalletCreated.copy( scanResponse = scanResponse.updateScanResponseAfterBackup(), ), @@ -247,13 +249,11 @@ internal class MultiWalletFinalizeModel @Inject constructor( } ?: userWalletCreated - userWalletsListManager.update( - userWalletId = userWallet.walletId, - update = { wallet -> - wallet.requireColdWallet().copy( - scanResponse = scanResponse.updateScanResponseAfterBackup(), - ) - }, + saveWalletUseCase( + userWallet = userWallet.requireColdWallet().copy( + scanResponse = scanResponse.updateScanResponseAfterBackup(), + ), + canOverride = true, ) userWallet diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index eab88630a0..00d3747298 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -20,10 +20,10 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT @@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, + val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { @@ -100,14 +101,14 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( childParams = childParams, onWalletCreated = { userWallet -> model.onWalletCreated(userWallet) - model.stackNavigation.push(OnboardingNoteRoute.TopUp) + model.stackNavigation.push(OnboardingNoteRoute.Done) }, ), ) - OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent( - appComponentContext = factoryContext, - params = OnboardingNoteTopUpComponent.Params( - childParams = childParams, + OnboardingNoteRoute.Done -> onboardingDoneComponentFactory.create( + context = factoryContext, + params = OnboardingDoneComponent.Params( + mode = OnboardingDoneComponent.Mode.WalletCreated, onDone = { params.onDone() }, ), tokenReceiveComponentFactory = tokenReceiveComponentFactory, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt deleted file mode 100644 index 771e29c209..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp -import com.tangem.features.tokenreceive.TokenReceiveComponent - -internal class OnboardingNoteTopUpComponent( - appComponentContext: AppComponentContext, - private val params: Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, -) : ComposableContentComponent, AppComponentContext by appComponentContext { - - private val model: OnboardingNoteTopUpModel = getOrCreateModel(params) - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = TokenReceiveConfig.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) - - OnboardingNoteTopUp( - modifier = modifier, - state = state, - ) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: TokenReceiveConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - - data class Params( - val childParams: DefaultOnboardingNoteComponent.ChildParams, - val onDone: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt deleted file mode 100644 index a8e760dbf3..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ /dev/null @@ -1,319 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.models.ReceiveAddressModel -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher -import com.tangem.domain.transaction.usecase.GetEnsNameUseCase -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM -import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.extensions.isPositive -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList") -@ModelScoped -internal class OnboardingNoteTopUpModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val rampStateManager: RampStateManager, - private val cardRepository: CardRepository, - private val saveWalletUseCase: SaveWalletUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, - private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, - private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, - private val getEnsNameUseCase: GetEnsNameUseCase, -) : Model() { - - private val params = paramsContainer.require() - private val commonState = params.childParams.commonState - private val scanResponse = params.childParams.commonState.value.scanResponse - private var userWallet = params.childParams.commonState.value.userWallet - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - private val _uiState = MutableStateFlow( - OnboardingNoteTopUpUM( - onRefreshBalanceClick = ::refreshBalance, - onBuyCryptoClick = ::onBuyCryptoClick, - onShowWalletAddressClick = ::onShowWalletAddressClick, - onDismissBottomSheet = ::onDismissBottomSheet, - ), - ) - - val uiState: StateFlow = _uiState - - init { - Analytics.send(OnboardingEvent.Topup.ScreenOpened) - observeArtwork() - modelScope.launch { - createUserWalletIfNull() - cardRepository.finishCardActivation(scanResponse.card.cardId) - observeCryptoCurrencyStatus() - refreshBalance() - } - } - - private fun refreshBalance() { - modelScope.launch { - showBalanceLoadingProgress(true) - createUserWalletIfNull() - val userWalletId = requireNotNull(userWallet?.walletId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true) - } - showBalanceLoadingProgress(false) - } - } - - private fun onBuyCryptoClick() { - val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - modelScope.launch { - getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight { - urlOpener.openUrl(it) - } - } - Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency)) - } - - private fun onShowWalletAddressClick() { - val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - val networkAddress = currencyStatus.value.networkAddress ?: return - - if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { - val userWalletId = userWallet?.walletId ?: return - modelScope.launch { - configureReceiveAddresses( - cryptoCurrencyStatus = currencyStatus, - userWalletId = userWalletId, - )?.let { bottomSheetNavigation.activate(it) } - } - } else { - _uiState.update { - it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) - } - } - Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) - } - - private fun onDismissBottomSheet() { - _uiState.update { - it.copy(addressBottomSheetConfig = null) - } - } - - private suspend fun createUserWalletIfNull() { - if (userWallet != null) { - return - } - val commonState = params.childParams.commonState.value - userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse) - } - - private fun observeArtwork() { - modelScope.launch { - params.childParams.commonState.collect { - _uiState.value = _uiState.value.copy( - cardArtwork = it.cardArtwork, - ) - } - } - } - - private fun observeCryptoCurrencyStatus() { - val userWalletId = userWallet?.walletId ?: return - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) - .map { it.getOrNull() } - .filterNotNull() - .onEach(::applyCryptoCurrencyStatusToState) - .launchIn(modelScope) - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - if (commonState.value.cryptoCurrencyStatus == null) { - loadAvailableForBuy(status) - } - - commonState.update { - it.copy(cryptoCurrencyStatus = status) - } - - val amount = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.amount - is CryptoCurrencyStatus.NoAccount -> status.value.amount - is CryptoCurrencyStatus.NoQuote -> status.value.amount - else -> null - } - val hasCurrentNetworkTransactions = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions - is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions - else -> false - } - val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount - - if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { - params.onDone() - } - - _uiState.update { - it.copy( - amountToCreateAccount = amountToCreateAccount - ?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }, - balance = amount?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }.orEmpty(), - isTopUpDataLoading = status.value.networkAddress == null, - ) - } - } - - private fun showBalanceLoadingProgress(value: Boolean) { - _uiState.update { - it.copy(isRefreshing = value) - } - } - - private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { - modelScope.launch { - val availableForBuy = rampStateManager.availableForBuy( - userWallet = userWallet ?: return@launch, - cryptoCurrency = cryptoCurrencyStatus.currency, - ) - _uiState.update { - it.copy( - availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None, - availableForBuyLoading = false, - ) - } - } - } - - private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) = - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = uiState.value.onDismissBottomSheet, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currencyStatus.currency.name, - symbol = currencyStatus.currency.symbol, - ), - network = currencyStatus.currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ) - - private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet { - val wallet = requireNotNull( - value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(), - lazyMessage = { "User wallet not created" }, - ) - saveWalletUseCase(wallet, false) - return wallet - } - - private suspend fun configureReceiveAddresses( - cryptoCurrencyStatus: CryptoCurrencyStatus, - userWalletId: UserWalletId, - ): TokenReceiveConfig? { - val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null - - val ensName = getEnsNameUseCase.invoke( - userWalletId = userWalletId, - network = cryptoCurrencyStatus.currency.network, - address = addresses.defaultAddress.value, - ) - - val receiveAddresses = buildList { - ensName?.let { ens -> - add( - ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Ens, - value = ens, - displayName = ens, - ), - ) - } - addresses.availableAddresses.map { address -> - add( - ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Default, - value = address.value, - displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", - ), - ) - } - } - - return TokenReceiveConfig( - shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), - cryptoCurrency = cryptoCurrencyStatus.currency, - userWalletId = userWalletId, - showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network - .TransactionExtrasType.NONE, - receiveAddress = receiveAddresses, - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt deleted file mode 100644 index 672d5e838b..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.LocalTangemShimmer -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.common.ui.RefreshButton -import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R -import com.valentinilk.shimmer.shimmer - -@Composable -fun OnboardingNoteTopUpHeader( - balance: String, - cardArtwork: ArtworkUM?, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .heightIn(min = 180.dp) - .widthIn(max = 450.dp), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 32.dp), - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - modifier = if (balance.isEmpty()) { - Modifier - .width(120.dp) - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) - .shimmer(LocalTangemShimmer.current) - } else { - Modifier - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - text = balance, - ) - SpacerHMax() - } - } - WalletCard( - modifier = Modifier.width(120.dp).align(Alignment.TopCenter), - artwork = cardArtwork, - ) - RefreshButton( - modifier = Modifier.align(Alignment.BottomCenter), - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardinNoteTopUpHeaderPreview() { - TangemThemePreview { - OnboardingNoteTopUpHeader( - balance = "0.00000001 BTC", - cardArtwork = ArtworkUM(null, ""), - onRefreshBalanceClick = {}, - isRefreshing = false, - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt deleted file mode 100644 index 476d06b378..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM - -@Composable -fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnboardingNoteTopUpHeader( - balance = state.balance, - cardArtwork = state.cardArtwork, - onRefreshBalanceClick = state.onRefreshBalanceClick, - isRefreshing = state.isRefreshing, - modifier = Modifier - .padding(top = 64.dp) - .padding(horizontal = 24.dp) - .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth(), - ) - Column( - modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 16.dp), - ) - - val text = if (state.amountToCreateAccount != null) { - stringResourceSafe( - R.string.onboarding_top_up_min_create_account_amount, - state.amountToCreateAccount, - ) - } else { - stringResourceSafe(R.string.onboarding_top_up_body) - } - SpacerH16() - Text( - text = text, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - - BottomButtons(state) - - state.addressBottomSheetConfig?.let { config -> - TokenReceiveBottomSheet(config = config) - } - } -} - -@Composable -private fun BottomButtons(state: OnboardingNoteTopUpUM) { - if (state.availableForBuy) { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - } else { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_button_receive_crypto), - onClick = state.onShowWalletAddressClick, - ) - } - AnimatedVisibility(visible = !state.availableForBuyLoading) { - if (state.availableForBuy) { - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowWalletAddressClick, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardingNoteTopUpPreview() { - TangemThemePreview { - OnboardingNoteTopUp( - state = OnboardingNoteTopUpUM( - availableForBuy = true, - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt deleted file mode 100644 index c9d9ca3541..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig - -data class OnboardingNoteTopUpUM( - val cardArtwork: ArtworkUM? = null, - val availableForBuy: Boolean = false, - val availableForBuyLoading: Boolean = true, - val balance: String = "", - val isRefreshing: Boolean = false, - val isTopUpDataLoading: Boolean = true, - val amountToCreateAccount: String? = null, - val addressBottomSheetConfig: TangemBottomSheetConfig? = null, - val onBuyCryptoClick: () -> Unit = {}, - val onShowWalletAddressClick: () -> Unit = {}, - val onRefreshBalanceClick: () -> Unit = {}, - val onDismissBottomSheet: () -> Unit = {}, -) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt index be064600cc..136a2ae24d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import dagger.Binds import dagger.Module @@ -37,9 +36,4 @@ internal interface ModelModule { @IntoMap @ClassKey(OnboardingNoteCreateWalletModel::class) fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model - - @Binds - @IntoMap - @ClassKey(OnboardingNoteTopUpModel::class) - fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 5d165cbe97..ae47a8ab9d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -84,7 +84,7 @@ internal class OnboardingNoteModel @Inject constructor( return if (card.wallets.isEmpty()) { OnboardingNoteRoute.CreateWallet } else { - OnboardingNoteRoute.TopUp + OnboardingNoteRoute.Done } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt index 1d7573e677..cc8101af19 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt @@ -9,7 +9,7 @@ internal sealed class OnboardingNoteRoute { data object CreateWallet : OnboardingNoteRoute() @Serializable - data object TopUp : OnboardingNoteRoute() + data object Done : OnboardingNoteRoute() } -internal const val ONBOARDING_NOTE_STEPS_COUNT = 3 \ No newline at end of file +internal const val ONBOARDING_NOTE_STEPS_COUNT = 2 \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt index 3df643e22b..83b41e6d8f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt @@ -2,5 +2,5 @@ package com.tangem.features.onboarding.v2.note.impl.route internal fun OnboardingNoteRoute.stepNum() = when (this) { OnboardingNoteRoute.CreateWallet -> 1 - OnboardingNoteRoute.TopUp -> 2 + OnboardingNoteRoute.Done -> 2 } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index f0076b3bee..ab86ceed99 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,7 +1,5 @@ package com.tangem.features.onboarding.v2.twin.impl.model -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -9,47 +7,26 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.datasource.local.config.issuers.IssuersConfigStorage -import com.tangem.domain.card.common.util.twinsIsTwinned import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.ReceiveAddressModel -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher -import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.impl.R @@ -58,7 +35,6 @@ import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM -import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -67,11 +43,9 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -80,33 +54,21 @@ internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, private val tangemSdkManager: TangemSdkManager, private val issuersConfigStorage: IssuersConfigStorage, private val cardRepository: CardRepository, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val tokensFeatureToggles: TokensFeatureToggles, - private val walletBalanceFetcher: WalletBalanceFetcher, - private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, - private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, - private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { private val params = paramsContainer.require() private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin") private val cryptoCurrencyStatusJobHolder = JobHolder() - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - private val _uiState = MutableStateFlow( when (params.mode) { Mode.WelcomeOnly -> { @@ -126,14 +88,10 @@ internal class OnboardingTwinModel @Inject constructor( ) } Mode.CreateWallet -> { - if (params.scanResponse.twinsIsTwinned()) { - OnboardingTwinUM.TopUpPrepare - } else { - OnboardingTwinUM.Welcome( - pairCardNumber = firstCardTwinNumber.pairNumber().number, - onContinueClick = ::navigateToFirstScan, - ) - } + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) } }, ) @@ -149,11 +107,6 @@ internal class OnboardingTwinModel @Inject constructor( saveTwinsOnboardingShownUseCase() } } - OnboardingTwinUM.TopUpPrepare -> { - modelScope.launch { - setTopUpState(params.scanResponse) - } - } else -> {} } } @@ -211,9 +164,9 @@ internal class OnboardingTwinModel @Inject constructor( // remove wallet only after first step of retwin if (params.mode == Mode.RecreateWallet) { - userWalletsListManager.delete( - listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), - ) + UserWalletIdBuilder.scanResponse(params.scanResponse).build()?.let { + deleteWalletUseCase(it) + } } analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) @@ -228,10 +181,7 @@ internal class OnboardingTwinModel @Inject constructor( }, ) } - - innerNavigationState.update { - it.copy(stackSize = 2) - } + innerNavigationState.update { it.copy(stackSize = 2) } } } } @@ -239,7 +189,6 @@ internal class OnboardingTwinModel @Inject constructor( private fun createSecondWallet(firstPublicKey: String) { setLoading(true) - modelScope.launch { val secondCardNumber = firstCardTwinNumber.pairNumber().number val result = tangemSdkManager.createSecondTwinWallet( @@ -328,142 +277,31 @@ internal class OnboardingTwinModel @Inject constructor( Mode.CreateWallet -> { modelScope.launch { setLoading(true) - setTopUpState(scanResponse) + finishActivation(scanResponse) }.saveIn(cryptoCurrencyStatusJobHolder) } } } - private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope { + private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope { val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run { Timber.e("User wallet not created") setLoading(false) return@coroutineScope } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@coroutineScope + } cardRepository.finishCardActivation(params.scanResponse.card.cardId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - } else { - fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true) - } - .onLeft { - Timber.e("Unable to fetch currency status: $it") - setLoading(false) - } - - val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .firstOrNull()?.getOrNull() - ?: run { - setLoading(false) - Timber.e("Unable to get currency status") - return@coroutineScope - } - - launch { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .collect { - it.onRight { status -> - applyCryptoCurrencyStatusToState(status) - } - } - } - - _uiState.value = OnboardingTwinUM.TopUp( - onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, - onRefreshClick = { onRefreshBalanceClick(userWallet) }, - onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, - isLoading = true, - ) - - innerNavigationState.update { - it.copy(stackSize = 4) - } - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return - if (amount > BigDecimal.ZERO) { - params.modelCallbacks.onDone() - } else { - update { - it.copy( - balance = BigDecimal.ZERO.format { crypto(status.currency) }, - onBuyCryptoClick = { onBuyCryptoClick(status) }, - onShowAddressClick = { onShowAddressClick(status) }, - isLoading = false, - ) - } - } - } - - private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { - modelScope.launch { - getLegacyTopUpUrlUseCase(status).onRight { - urlOpener.openUrl(it) - } - } - } - - private fun onShowAddressClick(status: CryptoCurrencyStatus) { - val currency = status.currency - val networkAddress = status.value.networkAddress ?: return - - if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { - modelScope.launch { - configureReceiveAddresses(cryptoCurrencyStatus = status)?.let { - bottomSheetNavigation.activate(it) - } - } - } else { - update { - it.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = { - update { - it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) - } - }, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currency.name, - symbol = currency.symbol, - ), - network = currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ), - ) - } - } - } - - private fun onRefreshBalanceClick(userWallet: UserWallet) { - update { - it.copy(isLoading = true) - } - modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true) - } - } + params.modelCallbacks.onDone() } private fun saveWalletAndDone() { @@ -476,7 +314,15 @@ internal class OnboardingTwinModel @Inject constructor( return@launch } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@launch + } + params.modelCallbacks.onDone() } } @@ -526,45 +372,4 @@ internal class OnboardingTwinModel @Inject constructor( TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard } - - private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? { - val userWallet = coldUserWalletBuilderFactory.create(params.scanResponse).build() ?: return null - val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null - - val ensName = getEnsNameUseCase.invoke( - userWalletId = userWallet.walletId, - network = cryptoCurrencyStatus.currency.network, - address = addresses.defaultAddress.value, - ) - - val receiveAddresses = buildList { - ensName?.let { ens -> - add( - ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Ens, - value = ens, - displayName = ens, - ), - ) - } - addresses.availableAddresses.map { address -> - add( - ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Default, - value = address.value, - displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", - ), - ) - } - } - - return TokenReceiveConfig( - shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), - cryptoCurrency = cryptoCurrencyStatus.currency, - userWalletId = userWallet.walletId, - showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network - .TransactionExtrasType.NONE, - receiveAddress = receiveAddresses, - ) - } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index c67669148b..d2e579d459 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH16 -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme @@ -43,11 +41,6 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi .weight(.48f) .fillMaxWidth(), state = state.artwork, - balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "", - isRefreshing = state.isLoading, - onRefreshBalanceClick = { - (state as? OnboardingTwinUM.TopUp)?.onRefreshClick() - }, ) AnimatedContent( @@ -60,16 +53,10 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi when (st) { is OnboardingTwinUM.ResetWarning -> ResetWarning(st) is OnboardingTwinUM.ScanCard -> ScanCard(st) - is OnboardingTwinUM.TopUp -> TopUp(st) is OnboardingTwinUM.Welcome -> Welcome(st) - OnboardingTwinUM.TopUpPrepare -> {} } } } - - if (state is OnboardingTwinUM.TopUp) { - TokenReceiveBottomSheet(config = state.bottomSheetConfig) - } } @Suppress("LongMethod") @@ -154,55 +141,6 @@ private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifie } } -@Composable -private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Column( - modifier = Modifier - .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - SpacerH16() - - Text( - text = stringResourceSafe(R.string.onboarding_top_up_body), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body1, - ) - } - - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowAddressClick, - ) - } -} - @Composable private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) { Column( @@ -287,14 +225,6 @@ private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifi } } -@Preview(showBackground = true) -@Composable -private fun PreviewTopUp() { - TangemThemePreview { - OnboardingTwin(OnboardingTwinUM.TopUp()) - } -} - @Preview(showBackground = true) @Composable private fun PreviewWelcome() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index 97758be4e1..3e0240af22 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -1,12 +1,10 @@ package com.tangem.features.onboarding.v2.twin.impl.ui +import android.annotation.SuppressLint import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -16,21 +14,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.zIndex -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.wallets.models.Artwork -import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit @@ -45,8 +37,6 @@ internal sealed class TwinWalletArtworkUM { FirstCard, SecondCard } } - - data object TopUp : TwinWalletArtworkUM() } private data class CardsTransitionState( @@ -64,15 +54,10 @@ private data class WalletCardTransitionState( val zIndex: Float = 0f, ) +@SuppressLint("UnusedBoxWithConstraintsScope") @Suppress("LongMethod") @Composable -internal fun TwinWalletArtworks( - state: TwinWalletArtworkUM, - balance: String, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun TwinWalletArtworks(state: TwinWalletArtworkUM, modifier: Modifier = Modifier) { BoxWithConstraints( modifier .heightIn(min = 180.dp) @@ -110,22 +95,6 @@ internal fun TwinWalletArtworks( } } - AnimatedVisibility( - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - ) - } - AnimatedTwinCards( transition1 = transition1, transition2 = transition2, @@ -133,46 +102,6 @@ internal fun TwinWalletArtworks( .widthIn(max = 450.dp) .matchParentSize(), ) - - AnimatedVisibility( - modifier = Modifier.align(Alignment.Center), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - text = balance, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - } - - AnimatedVisibility( - modifier = Modifier.align(Alignment.BottomCenter), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - RefreshButton( - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } } } @@ -308,26 +237,6 @@ private fun TwinWalletArtworkUM.toTransitionSetState( ) } } - TwinWalletArtworkUM.TopUp -> { - val scale = 0.4f - val yTranslation = -maxHeightDp * density - 24 * density - listOf( - CardsTransitionState( - walletCard1 = WalletCardTransitionState( - yTranslation = yTranslation, - xScale = scale, - yScale = scale, - zIndex = 2f, - ), - walletCard2 = WalletCardTransitionState( - yTranslation = yTranslation * 0.35f, - xScale = scale * 0.8f, - yScale = scale * 0.8f, - zIndex = 1f, - ), - ), - ) - } } @Preview(showBackground = true, widthDp = 360, heightDp = 640) @@ -341,16 +250,13 @@ private fun Preview() { .fillMaxSize(), contentAlignment = Alignment.Center, ) { - var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.Spread) } TwinWalletArtworks( state = state, modifier = Modifier .padding(top = 250.dp) .fillMaxWidth(), - balance = "1 USD", - isRefreshing = false, - onRefreshBalanceClick = {}, ) var index by remember { mutableIntStateOf(0) } @@ -366,7 +272,6 @@ private fun Preview() { TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), - TwinWalletArtworkUM.TopUp, ) state = list[index % list.size] diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt index 7341af8545..defa7831a4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.onboarding.v2.twin.impl.ui.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM @Immutable @@ -11,12 +10,6 @@ internal sealed class OnboardingTwinUM { abstract val isLoading: Boolean abstract val artwork: TwinWalletArtworkUM - data object TopUpPrepare : OnboardingTwinUM() { - override val stepIndex: Int = 0 - override val isLoading: Boolean = false - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread - } - data class Welcome( override val isLoading: Boolean = false, val pairCardNumber: Int = 2, @@ -56,23 +49,9 @@ internal sealed class OnboardingTwinUM { override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) } - data class TopUp( - override val isLoading: Boolean = false, - val balance: String = "", - val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, - val onBuyCryptoClick: () -> Unit = {}, - val onShowAddressClick: () -> Unit = {}, - val onRefreshClick: () -> Unit = {}, - ) : OnboardingTwinUM() { - override val stepIndex: Int = 2 - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp - } - fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { is Welcome -> copy(isLoading = isLoading) is ResetWarning -> copy() is ScanCard -> copy(isLoading = isLoading) - is TopUp -> copy(isLoading = isLoading) - TopUpPrepare -> this } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt index 5cd9a11f10..dff0ca06c5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -1,15 +1,11 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui -import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -19,9 +15,9 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.outsetBorder import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -112,17 +108,7 @@ private fun SelectableChainRow( RowContentContainer( modifier = modifier .heightIn(min = 48.dp) - .outsetBorder( - color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent, - width = 5.dp, - shape = RoundedCornerShape(size = 18.dp), - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .border( - width = 1.dp, - color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) + .selectedBorder(selected) .clickable(onClick = onClick) .padding(12.dp), icon = { @@ -163,7 +149,7 @@ private fun Preview() { ), ), selectedOption = SelectableChainRowUM( - event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet, + event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet, icon = R.drawable.ic_tangem_24, text = TextReference.Str("Tangem Wallet"), ), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 102098867b..e517a628cb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -20,8 +20,8 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent @@ -46,7 +46,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -173,7 +173,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } val userWallet = createUserWallet(params.scanResponse, newTokens) - userWalletsListManager.save(userWallet) + saveWalletUseCase(userWallet) visaAuthTokenStorage.remove(params.scanResponse.card.cardId) otpStorage.removeOTP(params.scanResponse.card.cardId) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt index cb2d5993c8..094a37d884 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -153,7 +153,7 @@ private fun PinCode( } }, keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, + keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 6259c9cacc..ae9b3f2e32 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -21,7 +21,6 @@ import com.tangem.domain.tokens.GetAssetRequirementsUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -210,21 +209,22 @@ internal class OnrampTokenListModel @Inject constructor( val isOperationAvailable = checkAvailabilityByOperation(status = status) val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation val isNotLoading = status.value !is CryptoCurrencyStatus.Loading + val requirements = getAssetRequirementsUseCase( userWalletId = userWallet.walletId, currency = status.currency, ).getOrNull() - val isNotTrustlineRequired = requirements !is AssetRequirementsCondition.RequiredTrustline + val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements) val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable val isAvailable = when (params.filterOperation) { OnrampOperation.BUY -> { - isNotTrustlineRequired + isAvailableForBuy } // unreachable state is available for Buy operation OnrampOperation.SELL -> isNotUnreachable OnrampOperation.SWAP -> { - isNotUnreachable && isNotTrustlineRequired + isNotUnreachable && isAvailableForBuy } } diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt index 4e152b9c09..2ab5d7832f 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt @@ -6,4 +6,5 @@ data class PushNotificationsParams( val isBottomSheet: Boolean = false, val nextRoute: AppRoute? = null, val modelCallbacks: PushNotificationsModelCallbacks, + val source: AppRoute.PushNotification.Source, ) \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt index 7977e16e18..8ffec6c23c 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -53,6 +53,15 @@ sealed class PushNotificationAnalyticEvents( ), ) + data class NotificationsScreenOpened( + val source: AnalyticsParam.ScreensSources, + ) : PushNotificationAnalyticEvents( + event = "Push Notification Screen Opened", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), + ) + data class NotificationsEnabled( val isEnabled: Boolean, ) : PushNotificationAnalyticEvents( diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index 9a6115aa48..c76c250f14 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.pushnotifications.impl.model import androidx.compose.runtime.Stable +import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -36,6 +37,11 @@ internal class PushNotificationsModel @Inject constructor( ) : Model(), PushNotificationsClickIntents { val params: PushNotificationsParams = paramsContainer.require() + val source = when (params.source) { + AppRoute.PushNotification.Source.Stories -> AnalyticsParam.ScreensSources.Stories + AppRoute.PushNotification.Source.Main -> AnalyticsParam.ScreensSources.Main + AppRoute.PushNotification.Source.Onboarding -> AnalyticsParam.ScreensSources.Onboarding + } private val _state = MutableStateFlow( PushNotificationsUM( @@ -43,6 +49,10 @@ internal class PushNotificationsModel @Inject constructor( ), ) + init { + analyticHandler.send(PushNotificationAnalyticEvents.NotificationsScreenOpened(source)) + } + val state = _state.asStateFlow() override fun onAllowClick() { @@ -51,9 +61,7 @@ internal class PushNotificationsModel @Inject constructor( notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true) } } - analyticHandler.send( - PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), - ) + analyticHandler.send(PushNotificationAnalyticEvents.ButtonAllow(source)) } override fun onLaterClick() { @@ -62,9 +70,7 @@ internal class PushNotificationsModel @Inject constructor( notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false) } } - analyticHandler.send( - PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories), - ) + analyticHandler.send(PushNotificationAnalyticEvents.ButtonLater(source)) modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index fb665394be..294674ae19 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -3,5 +3,6 @@ package com.tangem.features.send.v2.api interface SendFeatureToggles { val isSendRedesignEnabled: Boolean + val isNFTSendRedesignEnabled: Boolean val isSendWithSwapEnabled: Boolean } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index ca50585ecf..89ddb338b6 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -5,8 +5,10 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.R import com.tangem.features.send.v2.api.entity.FeeItem.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -72,14 +74,37 @@ sealed class FeeNonce { @Immutable sealed class FeeItem { abstract val fee: Fee + abstract val title: TextReference + abstract val iconRes: Int fun isSameClass(other: FeeItem): Boolean { return this::class == other::class } - data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem() - data class Slow(override val fee: Fee) : FeeItem() - data class Market(override val fee: Fee) : FeeItem() - data class Fast(override val fee: Fee) : FeeItem() - data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() + data class Suggested( + override val title: TextReference, + override val fee: Fee, + ) : FeeItem() { + override val iconRes: Int = R.drawable.ic_star_mini_24 + } + + data class Slow(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_slow) + override val iconRes: Int = R.drawable.ic_tortoise_24 + } + + data class Market(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_market) + override val iconRes: Int = R.drawable.ic_bird_24 + } + + data class Fast(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_fast) + override val iconRes: Int = R.drawable.ic_hare_24 + } + + data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_custom) + override val iconRes: Int = R.drawable.ic_edit_v2_24 + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index f414e52610..d73b45c69b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -8,6 +8,8 @@ internal class DefaultSendFeatureToggles( ) : SendFeatureToggles { override val isSendRedesignEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") + override val isNFTSendRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("NFT_SEND_REDESIGN_ENABLED") override val isSendWithSwapEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED") } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt new file mode 100644 index 0000000000..473a7b235e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R + +@Composable +internal fun FeeBlock(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + val feeExtraInfo = feeSelectorUM.feeExtraInfo + val feeFiatRateUM = feeSelectorUM.feeFiatRateUM + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount + SelectorRowItem( + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, + preDot = remember { + stringReference( + feeAmount.value.format { + crypto( + symbol = feeAmount.currencySymbol, + decimals = feeAmount.decimals, + ).fee(canBeLower = feeExtraInfo.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) { + getFiatReference(feeAmount.value, feeFiatRateUM.rate, feeFiatRateUM.appCurrency) + } else { + null + } + }, + ellipsizeOffset = feeAmount.currencySymbol.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 289b609dff..610020a85c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -5,17 +5,19 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.* +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.send.confirm.SendConfirmComponent -import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent @Composable internal fun SendContent( @@ -34,9 +36,9 @@ internal fun SendContent( Children( stack = stackState, animation = stackAnimation { child -> - when (child.instance) { - is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f) - is SendConfirmComponent -> fade() + when (child.configuration) { + is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f) + is CommonSendRoute.Confirm -> fade() else -> slide() } }, @@ -45,7 +47,14 @@ internal fun SendContent( it.instance.Content(Modifier.weight(1f)) } if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) { - SendNavigationButtons(navigationUM = navigationUM) + NavigationButtonsBlockV2( + navigationUM = navigationUM, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt deleted file mode 100644 index 31cf103e3d..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -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.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.res.vectorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.ui.navigationButtons.NavigationButton -import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 -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.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.singleEvent - -@Composable -internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return - - Column( - modifier = modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - SendDoneButtons(navigationUM.secondaryPairButtonsUM) - SendNavigationButton( - navigationUM = navigationUM, - ) - } -} - -@Composable -private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - val navigationUM = navigationUM as? NavigationUM.Content ?: return - val primaryButton = navigationUM.primaryButton - - Row(modifier = modifier) { - AnimatedVisibility( - visible = navigationUM.prevButton != null, - enter = expandHorizontally(expandFrom = Alignment.End), - exit = shrinkHorizontally(shrinkTowards = Alignment.End), - ) { - val wrappedNavigationUM = remember(this) { requireNotNull(navigationUM.prevButton) } - Row { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_back_24)), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.button.secondary) - .clickableSingle(onClick = wrappedNavigationUM.onClick) - .padding(12.dp), - ) - SpacerW12() - } - } - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = { - if (primaryButton.isHapticClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - primaryButton.onClick() - }, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - ) - } -} - -@Composable -private fun SendDoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row(modifier = Modifier.padding(bottom = 12.dp)) { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = leftButton.iconRes!!, - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = rightButton.iconRes!!, - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt deleted file mode 100644 index da368be38b..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme - -@Composable -internal fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { - var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } - val keyboard by keyboardAsState() - - // the text should appear when the keyboard is closed - LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { - if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { - return@LaunchedEffect - } - isVisibleProxy = footerText != TextReference.EMPTY - } - - AnimatedVisibility( - visible = isVisibleProxy, - modifier = modifier, - enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), - exit = fadeOut(tween(durationMillis = 300)), - label = "Animate show sending state text", - ) { - Text( - text = footerText.resolveAnnotatedReference(), - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - ) - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 7bfbe898e3..5cb0f113e0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -18,6 +18,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent +import com.tangem.utils.extensions.isSingleItem import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -73,11 +74,13 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( val state by model.uiState.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() + val isScreenSource = params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen + val isNotSingleFee = (state as? FeeSelectorUM.Content)?.feeItems?.isSingleItem() == false FeeSelectorBlockContent( state = state, onReadMoreClick = model::onReadMoreClicked, modifier = modifier - .conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) { + .conditional(isScreenSource && isNotSingleFee) { Modifier.clickable { model.showFeeSelector() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index b0f08978aa..2f403f5b0c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -39,6 +39,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.impl.R +import com.tangem.utils.extensions.isSingleItem import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -118,12 +119,11 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie text = annotatedString, modifier = Modifier .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), + .size(TangemTheme.dimens.size16) + .clip(CircleShape), content = { contentModifier -> Icon( - modifier = contentModifier - .size(TangemTheme.dimens.size16) - .clip(CircleShape), + modifier = contentModifier.size(TangemTheme.dimens.size16), painter = painterResource(id = R.drawable.ic_token_info_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -176,12 +176,14 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi textAlign = TextAlign.End, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) - Icon( - modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource(id = R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) + if (!state.feeItems.isSingleItem()) { + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } } } @@ -220,7 +222,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider { ), FeeSelectorUM.Content( isPrimaryButtonEnabled = false, - feeItems = persistentListOf(maxFeeItem), + feeItems = persistentListOf(lowFeeItem, maxFeeItem), selectedFeeItem = maxFeeItem, feeExtraInfo = FeeExtraInfo( isFeeApproximate = false, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 5b1406dd0b..c99b647b80 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -79,7 +79,7 @@ internal fun FeeSelectorModalBottomSheet( FeeSelectorItems( state = state, feeSelectorIntents = feeSelectorIntents, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 13.dp), + modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) }, footer = { @@ -115,7 +115,6 @@ private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDis } } -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun FeeSelectorItems( state: FeeSelectorUM.Content, @@ -141,114 +140,9 @@ private fun FeeSelectorItems( ) val itemModifier = Modifier .fillMaxWidth() - .background(TangemTheme.colors.background.primary) .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { - is FeeItem.Suggested -> RegularFeeItemContent( - modifier = itemModifier, - title = item.title, - iconRes = R.drawable.ic_star_mini_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Slow -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_slow), - iconRes = R.drawable.ic_tortoise_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Market -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_market), - iconRes = R.drawable.ic_bird_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Fast -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_fast), - iconRes = R.drawable.ic_hare_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) is FeeItem.Custom -> CustomFeeBlock( modifier = itemModifier, customFee = item, @@ -258,6 +152,32 @@ private fun FeeSelectorItems( onValueChange = feeSelectorIntents::onCustomFeeValueChange, nonce = state.feeNonce, ) + else -> RegularFeeItemContent( + modifier = itemModifier, + title = item.title, + iconRes = item.iconRes, + iconBackgroundColor = iconBackgroundColor, + iconTint = iconTint, + preDot = stringReference( + item.fee.amount.value.format { + crypto( + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + }, + ), + postDot = if (feeFiatRateUM != null) { + getFiatReference( + value = item.fee.amount.value, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, + ) + } else { + null + }, + ellipsizeOffset = item.fee.amount.currencySymbol.length, + showDivider = !isSelected && !lastItem, + ) } } } @@ -488,11 +408,21 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< isPrimaryButtonEnabled = false, feeItems = persistentListOf( FeeItem.Suggested( - title = stringReference("Suggested by Tangem"), + title = resourceReference( + id = R.string.wc_fee_suggested, + formatArgs = wrappedList("Tangem"), + ), fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), ), FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), - FeeItem.Market(fee = Fee.Common(Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum))), + FeeItem.Market( + fee = Fee.Common( + Amount( + value = BigDecimal("0.02"), + blockchain = Blockchain.Ethereum, + ), + ), + ), FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 47c52407c5..74c7fa09ce 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -38,7 +38,6 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted @@ -156,7 +155,7 @@ internal class DefaultSendComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, @@ -254,7 +253,6 @@ internal class DefaultSendComponent @AssistedInject constructor( val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value - val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value if (sendAmount == null || destinationAddress == null || @@ -279,29 +277,10 @@ internal class DefaultSendComponent @AssistedInject constructor( onClick = {}, ) - val feeBlockComponent = SendFeeBlockComponent( - appComponentContext = child("sendConfirmFeeBlock"), - params = SendFeeComponentParams.FeeBlockParams( - state = model.uiState.value.feeUM, - analyticsCategoryName = model.analyticCategoryName, - userWallet = model.userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - appCurrency = model.appCurrency, - sendAmount = sendAmount, - destinationAddress = destinationAddress, - blockClickEnableFlow = MutableStateFlow(true), - onLoadFee = model::loadFee, - ), - onResult = { }, - onClick = {}, - ) - return SendConfirmSuccessComponent( appComponentContext = factoryContext, params = SendConfirmSuccessComponent.Params( sendUMFlow = model.uiState, - feeBlockComponent = feeBlockComponent, destinationBlockComponent = destinationBlockComponent, analyticsCategoryName = model.analyticCategoryName, currentRoute = model.currentRoute, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index a458d480c5..1fb318f5c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -632,7 +632,8 @@ internal class SendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, onClick = { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 73b2a5a0fa..7f7c70235b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference @@ -22,7 +23,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.FeeSelectorBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt index c9fa321928..5468cbf45e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt @@ -12,7 +12,6 @@ import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel import com.tangem.features.send.v2.send.success.ui.SendConfirmSuccessContent import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +22,6 @@ internal class SendConfirmSuccessComponent( private val model: SendConfirmSuccessModel = getOrCreateModel(params = params) private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent - private val feeBlockComponent: SendFeeBlockComponent = params.feeBlockComponent @Composable override fun Content(modifier: Modifier) { @@ -31,14 +29,12 @@ internal class SendConfirmSuccessComponent( SendConfirmSuccessContent( sendUM = state, destinationBlockComponent = destinationBlockComponent, - feeBlockComponent = feeBlockComponent, ) } data class Params( val sendUMFlow: StateFlow, val destinationBlockComponent: SendDestinationBlockComponent, - val feeBlockComponent: SendFeeBlockComponent, val analyticsCategoryName: String, val currentRoute: Flow, val txUrl: String, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 862d4e1483..1d679a3e59 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -1,17 +1,18 @@ package com.tangem.features.send.v2.send.success.ui import androidx.compose.animation.* +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.ui.AmountBlock -import com.tangem.core.ui.components.SpacerHMax +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -20,19 +21,14 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.common.ui.SendNavigationButtons +import com.tangem.features.send.v2.common.ui.FeeBlock import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.delay @Composable -internal fun SendConfirmSuccessContent( - sendUM: SendUM, - destinationBlockComponent: SendDestinationBlockComponent, - feeBlockComponent: SendFeeBlockComponent, -) { +internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -50,13 +46,17 @@ internal fun SendConfirmSuccessContent( exit = slideOutVertically().plus(fadeOut()), label = "Animate success content", ) { - Column { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { Column( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .scrollable( state = rememberScrollState(), - orientation = Orientation.Horizontal, + orientation = Orientation.Vertical, ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -80,10 +80,20 @@ internal fun SendConfirmSuccessContent( onClick = {}, ) destinationBlockComponent.Content(modifier = Modifier) - feeBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = sendUM.feeSelectorUM) + Spacer(Modifier.height(60.dp)) } - SpacerHMax() - SendNavigationButtons(navigationUM = sendUM.navigationUM) + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index c4ecdee6e2..b23440cfeb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -18,7 +18,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -29,6 +28,7 @@ import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams @@ -42,7 +42,8 @@ import java.math.BigDecimal internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, - private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory, + private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory, private val analyticsEventHandler: AnalyticsEventHandler, ) : NFTSendComponent, AppComponentContext by appComponentContext { @@ -121,6 +122,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( is CommonSendRoute.Destination -> getDestinationComponent(factoryContext) is CommonSendRoute.Fee -> getFeeComponent(factoryContext) CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext) else -> getStubComponent() } @@ -164,9 +166,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( } } - private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent( + private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create( appComponentContext = factoryContext, - nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, params = NFTSendConfirmComponent.Params( state = model.uiState.value, analyticsCategoryName = analyticsCategoryName, @@ -180,9 +181,34 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( currentRoute = model.currentRouteFlow.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, onLoadFee = model::loadFee, + onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) }, ), ) + private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl + + if (txUrl == null) { + model.showAlertError() + return getStubComponent() + } + + return nftSendSuccessComponentFactory.create( + appComponentContext = factoryContext, + params = NFTSendSuccessComponent.Params( + nftSendUMFlow = model.uiState, + analyticsCategoryName = analyticsCategoryName, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + callback = model, + currentRoute = model.currentRouteFlow.filterIsInstance(), + txUrl = txUrl, + ), + ) + } + private fun getStubComponent() = ComposableContentComponent { } private fun onChildBack() { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index b9e53dda49..b57e306719 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -5,9 +5,10 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.Companion.NFT_SEND_CATEGORY import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import javax.inject.Inject @@ -34,7 +35,7 @@ internal class NFTSendAnalyticHelper @Inject constructor( Basic.TransactionSent( sentFrom = AnalyticsParam.TxSentFrom.NFT( blockchain = cryptoCurrency.network.name, - token = cryptoCurrency.symbol, + token = NFT_SEND_CATEGORY, // should send "NFT" in token param feeType = feeType, ), memoType = getSendTransactionMemoType(destinationUM?.memoTextField), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index cc0fe9a936..d2fd5670b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -10,17 +10,23 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM @@ -28,13 +34,17 @@ import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinat import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* import java.math.BigDecimal -internal class NFTSendConfirmComponent( - appComponentContext: AppComponentContext, - params: Params, +internal class NFTSendConfirmComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: NFTSendConfirmModel = getOrCreateModel(params = params) @@ -74,12 +84,28 @@ internal class NFTSendConfirmComponent( onClick = model::showEditFee, ) + private val feeSelectorBlockComponent = feeSelectorComponentFactory.create( + context = child("NFTSendConfirmFeeSelectorBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = params.onLoadFee, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, + ), + onResult = model::onFeeResult, + ) + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( context = child("NFTDetailsBlock"), params = NFTDetailsBlockComponent.Params( userWalletId = params.userWallet.walletId, nftAsset = params.nftAsset, nftCollectionName = params.nftCollectionName, + isSuccessScreen = false, + title = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)), ), ) @@ -126,6 +152,7 @@ internal class NFTSendConfirmComponent( nftSendUM = state, destinationBlockComponent = destinationBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, notificationsComponent = notificationsComponent, notificationsUM = notificationState, @@ -145,9 +172,15 @@ internal class NFTSendConfirmComponent( val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val onLoadFee: suspend () -> Either, + val onSendTransaction: () -> Unit, ) interface ModelCallback { fun onResult(nftSendUM: NFTSendUM) } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendConfirmComponent + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index ae95518260..d834d13885 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -33,6 +33,7 @@ import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger @@ -61,6 +62,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @Suppress("LongParameterList", "LargeClass") @ModelScoped @@ -89,7 +91,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val sendFeeReloadTrigger: SendFeeReloadTrigger, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, -) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback { +) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback { private val params: NFTSendConfirmComponent.Params = paramsContainer.require() @@ -141,6 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor( updateConfirmNotifications() } + override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } updateConfirmNotifications() @@ -400,13 +408,23 @@ internal class NFTSendConfirmModel @Inject constructor( ).onEach { (state, _) -> val confirmUM = state.confirmUM val confirmUMContent = confirmUM as? ConfirmUM.Content - val isReadyToSend = confirmUMContent != null && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( title = resourceReference(R.string.nft_send), - subtitle = confirmUMContent?.walletName, - backIconRes = R.drawable.ic_close_24, + subtitle = if (uiState.value.isRedesignEnabled) { + null + } else { + confirmUMContent?.walletName + }, + backIconRes = if (state.isRedesignEnabled) { + when (confirmUM) { + is ConfirmUM.Success -> R.drawable.ic_close_24 + else -> R.drawable.ic_back_24 + } + } else { + R.drawable.ic_close_24 + }, backIconClick = { analyticsEventHandler.send( CommonSendAnalyticEvents.CloseButtonClicked( @@ -416,25 +434,13 @@ internal class NFTSendConfirmModel @Inject constructor( isValid = confirmUM.isPrimaryButtonEnabled, ), ) - appRouter.pop() + if (state.isRedesignEnabled) { + router.pop() + } else { + appRouter.pop() + } }, - primaryButton = NavigationButton( - textReference = when (confirmUM) { - is ConfirmUM.Success -> resourceReference(R.string.common_close) - is ConfirmUM.Content -> if (confirmUM.isSending) { - resourceReference(R.string.send_sending) - } else { - resourceReference(R.string.common_send) - } - else -> resourceReference(R.string.common_send) - }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, - isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = isReadyToSend, - onClick = { - onNextClick(confirmUM) - }, - ), + primaryButton = primaryButtonUM(), prevButton = null, secondaryPairButtonsUM = ( NavigationButton( @@ -453,21 +459,40 @@ internal class NFTSendConfirmModel @Inject constructor( }.launchIn(modelScope) } - private fun onNextClick(confirmUM: ConfirmUM) { - when (confirmUM) { - is ConfirmUM.Success -> { - modelScope.launch { - nftSendSuccessTrigger.triggerSuccessNFTSend() + private fun primaryButtonUM(): NavigationButton { + val confirmUM = uiState.value.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending + return NavigationButton( + textReference = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) } - appRouter.pop() - } - is ConfirmUM.Content -> if (confirmUM.isSending) { - return - } else { - onSendClick() - } - else -> return - } + else -> resourceReference(R.string.common_send) + }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = isReadyToSend, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> { + modelScope.launch { + nftSendSuccessTrigger.triggerSuccessNFTSend() + } + appRouter.pop() + } + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ) } private companion object { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 9dbb04eb6d..9ff84edfb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.sendnft.confirm.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -9,7 +10,9 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle @@ -20,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -40,6 +43,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { @@ -54,6 +58,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent = destinationBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { tapHelp(isDisplay = confirmUM.showTapHelp) @@ -79,31 +84,45 @@ private fun LazyListScope.blocks( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, ) { item(key = BLOCKS_KEY) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - AnimatedVisibility( - visible = nftSendUM.confirmUM is ConfirmUM.Success, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) { - val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } - TransactionDoneTitle( - title = resourceReference(R.string.sent_transaction_sent_title), - subtitle = resourceReference( - R.string.send_date_format, - wrappedList( - wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), - wrappedConfirmUM.transactionDate.toTimeFormat(), - ), - ), - modifier = Modifier.padding(vertical = 12.dp), + if (nftSendUM.isRedesignEnabled) { + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), ) + } else { + TransactionDoneTitleAnimated(nftSendUM = nftSendUM) + destinationBlockComponent.Content(modifier = Modifier) + nftDetailsBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) } - destinationBlockComponent.Content(modifier = Modifier) - - nftDetailsBlockComponent.Content(modifier = Modifier) - - feeBlockComponent.Content(modifier = Modifier) } } +} + +@Composable +private fun TransactionDoneTitleAnimated(nftSendUM: NFTSendUM) { + AnimatedVisibility( + visible = nftSendUM.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = 12.dp), + ) { + val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt index 3252c7bc71..5980fa7d22 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +24,9 @@ internal interface NFTSendModelModule { @IntoMap @ClassKey(NFTSendConfirmModel::class) fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model + + @Binds + @IntoMap + @ClassKey(NFTSendSuccessModel::class) + fun provideNFTSendSuccessModel(model: NFTSendSuccessModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 0bebb6f107..90a8753b4a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -29,6 +29,8 @@ import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute @@ -36,6 +38,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -70,7 +73,8 @@ internal class NFTSendModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val alertFactory: SendConfirmAlertFactory, -) : Model(), SendNFTComponentCallback { + private val sendFeatureToggles: SendFeatureToggles, +) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -124,7 +128,7 @@ internal class NFTSendModel @Inject constructor( } else { when (currentRouteFlow.value) { is Destination -> router.push(Confirm) - Confirm -> router.push(ConfirmSuccess) + Confirm -> router.replaceAll(ConfirmSuccess) else -> onBackClick() } } @@ -193,6 +197,13 @@ internal class NFTSendModel @Inject constructor( } } + fun showAlertError() { + alertFactory.getGenericErrorState( + onFailedTxEmailClick = ::onFailedTxEmailClick, + popBack = router::pop, + ) + } + private fun onFailedTxEmailClick(errorMessage: String? = null) { saveBlockchainErrorUseCase( error = BlockchainErrorInfo( @@ -249,7 +260,9 @@ internal class NFTSendModel @Inject constructor( private fun initialState(): NFTSendUM = NFTSendUM( destinationUM = DestinationUM.Empty(), feeUM = FeeUM.Empty(), + feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, + isRedesignEnabled = sendFeatureToggles.isNFTSendRedesignEnabled, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt new file mode 100644 index 0000000000..76f7329658 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt @@ -0,0 +1,96 @@ +package com.tangem.features.send.v2.sendnft.success + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel +import com.tangem.features.send.v2.sendnft.success.ui.NFTSendSuccessContent +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal class NFTSendSuccessComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, + nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: NFTSendSuccessModel = getOrCreateModel(params = params) + + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( + context = child("NFTDetailsSuccessBlock"), + params = NFTDetailsBlockComponent.Params( + userWalletId = params.userWallet.walletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + isSuccessScreen = true, + title = resourceReference(R.string.nft_asset), + ), + ) + + private val sendDestinationBlockComponent = sendDestinationBlockComponentFactory.create( + context = child("NFTDestinationSuccessBlock"), + params = DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = MutableStateFlow(false), + predefinedValues = PredefinedValues.Empty, + ), + onResult = {}, + onClick = {}, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + NFTSendSuccessContent( + nftSendUM = state, + destinationBlockComponent = sendDestinationBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + modifier = modifier, + ) + } + + data class Params( + val nftSendUMFlow: StateFlow, + val analyticsCategoryName: String, + val currentRoute: Flow, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val userWallet: UserWallet, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val txUrl: String, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onResult(nftSendUM: NFTSendUM) + } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendSuccessComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt new file mode 100644 index 0000000000..9352ec6898 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt @@ -0,0 +1,107 @@ +package com.tangem.features.send.v2.sendnft.success.model + +import androidx.compose.runtime.Stable +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@Stable +@ModelScoped +internal class NFTSendSuccessModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, +) : Model() { + private val params: NFTSendSuccessComponent.Params = paramsContainer.require() + + val uiState = params.nftSendUMFlow + + init { + configConfirmSuccessNavigation() + } + + private fun configConfirmSuccessNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).filter { it.second is CommonSendRoute.ConfirmSuccess }.onEach { (state, _) -> + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = stringReference(""), + subtitle = null, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = true, + ), + ) + appRouter.pop() + }, + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_close), + iconRes = null, + isEnabled = true, + isHapticClick = false, + onClick = { + appRouter.pop() + }, + ), + prevButton = null, + secondaryPairButtonsUM = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + onClick = ::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + onClick = ::onShareClick, + ), + ), + ), + ) + }.launchIn(modelScope) + } + + private fun onExploreClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName)) + urlOpener.openUrl(params.txUrl) + } + + private fun onShareClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName)) + shareManager.shareText(params.txUrl) + } + + interface ModelCallback { + fun onResult(sendUM: SendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt new file mode 100644 index 0000000000..10fa100bc9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -0,0 +1,103 @@ +package com.tangem.features.send.v2.sendnft.success.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toPx +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.common.ui.FeeBlock +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import kotlinx.coroutines.delay + +@Composable +internal fun NFTSendSuccessContent( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + modifier: Modifier = Modifier, +) { + var visible by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(ANIMATION_DELAY) + visible = true + } + + val height = ANIMATION_OFFSET.toPx().toInt() + + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { height }, + ).plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate success content", + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .scrollable( + state = rememberScrollState(), + orientation = Orientation.Vertical, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (nftSendUM.confirmUM is ConfirmUM.Success) { + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + nftSendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + nftSendUM.confirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM) + Spacer(Modifier.height(60.dp)) + } + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = nftSendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } + } +} + +private const val ANIMATION_DELAY = 600L +private val ANIMATION_OFFSET = (-40).dp \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index fb48eed197..45287e3c6e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,13 +1,16 @@ package com.tangem.features.send.v2.sendnft.ui.state import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal data class NFTSendUM( val destinationUM: DestinationUM, val feeUM: FeeUM, + val feeSelectorUM: FeeSelectorUM, val confirmUM: ConfirmUM, val navigationUM: NavigationUM, + val isRedesignEnabled: Boolean, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt index 489d2d0bc6..7dc514bb33 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt @@ -10,4 +10,7 @@ internal enum class EnterAddressSource { val isPasted: Boolean get() = this != InputField + + val isAutoNext: Boolean + get() = this == RecentAddress || this == MyWallets } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index b457df2b9a..d68153cd9e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -286,8 +286,7 @@ internal class SendDestinationModel @Inject constructor( } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { - val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress && isValidMemo) { + if (type?.isAutoNext == true && isValidAddress && isValidMemo) { saveResult() (params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt index 10284bde92..5aae6a80e5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -14,6 +15,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN @@ -62,20 +64,24 @@ internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 } SelectorRowItem( - titleRes = title, + title = resourceReference(title), iconRes = icon, - preDot = stringReference( - feeAmount?.value.format { - crypto( - symbol = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - ).fee(canBeLower = feeUM.isFeeApproximate) - }, - ), - postDot = if (feeUM.isFeeConvertibleToFiat) { - getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) - } else { - null + preDot = remember { + stringReference( + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeUM.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeUM.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) + } else { + null + } }, ellipsizeOffset = feeAmount?.currencySymbol?.length, isSelected = true, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt index 3cc6d6042b..09ae784965 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt @@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN import com.tangem.core.ui.format.bigdecimal.crypto @@ -52,7 +53,7 @@ internal fun SendSpeedSelectorItem( .clickable { onSelect() }, ) { SelectorRowItem( - titleRes = titleRes, + title = resourceReference(titleRes), iconRes = iconRes, onSelect = onSelect, modifier = modifier, diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt index 6eece69628..020d3afcab 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt @@ -205,6 +205,7 @@ class SendConfirmationNotificationsTransformerTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 873cfb305a..6903fc2550 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -204,6 +204,7 @@ class SendConfirmationNotificationsTransformerV2Test { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt index 79cba32b78..b07143a6b1 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt @@ -307,6 +307,7 @@ class TransformersComparisonTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 36a4a319f5..4be332b317 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -38,6 +38,7 @@ internal data class BalanceState( val validator: Yield.Validator?, val pendingActions: ImmutableList, val isPending: Boolean, + val validatorAddress: String?, ) @Immutable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 15cc4b24ae..5d548f7ab4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -68,6 +68,7 @@ internal class BalanceItemConverter( pendingActions = value.pendingActions.toPersistentList(), isClickable = value.isClickable(), isPending = value.isPending, + validatorAddress = value.validatorAddress, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 3643ba7e27..4c47f26565 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -2,8 +2,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.stringReference 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.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -69,11 +69,12 @@ internal class RewardsValidatorStateConverter( }, ) val formattedFiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ) return BalanceState( @@ -91,6 +92,7 @@ internal class RewardsValidatorStateConverter( isClickable = true, type = balance.type, isPending = balance.isPending, + validatorAddress = balance.validatorAddress, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index c54feee187..d4f2c4da10 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -57,8 +57,10 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( val state = stateController.value val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data ?: error("Illegal state") - val validatorState = state.validatorState as? StakingStates.ValidatorState.Data - ?: error("No validator provided") + + val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address + ?: state.balanceState?.validatorAddress + ?: error("No validator address provided") val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") @@ -66,8 +68,6 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( val pendingAction = confirmationState.pendingAction val pendingActions = confirmationState.pendingActions - val validatorAddress = validatorState.chosenValidator.address - val isEnter = state.actionType is StakingActionCommonType.Enter val isApprovalNeeded = confirmationState.isApprovalNeeded val isAllowanceNotEnough = confirmationState.allowance < amount diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index da70fc8533..4deacdff09 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -100,6 +100,7 @@ internal object InitialStakingStatePreview { type = BalanceType.STAKED, subtitle = null, isPending = false, + validatorAddress = "", ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 9c1ea68323..4c0166c007 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -13,8 +13,6 @@ import com.tangem.features.staking.impl.presentation.state.utils.getPendingActio import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal class SetButtonsStateTransformer( private val urlOpener: UrlOpener, @@ -23,12 +21,13 @@ internal class SetButtonsStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl val buttonsState = if (prevState.isButtonsVisible()) { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), prevButton = getPrevButton(prevState), - extraButtons = getExtraButtons(prevState), - txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, + extraButtons = getExtraButtons(prevState).takeIf { txUrl != null }, + txUrl = txUrl, onTextClick = urlOpener::openUrl, ) } else { @@ -77,26 +76,23 @@ internal class SetButtonsStateTransformer( ).takeIf { prevState.currentStep.isPrevButtonVisible() } } - private fun getExtraButtons(prevState: StakingUiState): ImmutableList { - return persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_web_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onExploreClick, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_share_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onShareClick, - ), + private fun getExtraButtons(prevState: StakingUiState): Pair { + return NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onShareClick, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 9a319caa8a..47475d3b74 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -29,6 +29,7 @@ import com.tangem.features.staking.impl.presentation.state.converters.RewardsVal import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.PersistentList @@ -227,6 +228,9 @@ internal class SetInitialDataStateTransformer( } private fun getAprRange(validators: List): TextReference { + if (validators.isEmpty()) { + return stringReference(DASH_SIGN) + } val aprValues = validators .filter { it.preferred } .takeIf { it.isNotEmpty() } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index c70f2da7a7..c39ac9a72f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -6,8 +6,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList 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.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.R @@ -38,11 +38,12 @@ internal class ShowApprovalBottomSheetTransformer( val feeCryptoValue = fee.amount.value.format { crypto(fee.amount.currencySymbol, fee.amount.decimals) } - val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), - fiatCurrencyCode = appCurrencyProvider().code, - fiatCurrencySymbol = appCurrencyProvider().symbol, - ) + val feeFiatValue = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value).format { + fiat( + fiatCurrencyCode = appCurrencyProvider().code, + fiatCurrencySymbol = appCurrencyProvider().symbol, + ) + } return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( isShown = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index d9948ed3b8..c940c12e8a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -33,6 +33,10 @@ internal class ValidatorSelectChangeTransformer( selectedValidator } + if (selectedValidator == null && yield.preferredValidators.isEmpty()) { + return prevState + } + return prevState.copy( validatorState = StakingStates.ValidatorState.Data( chosenValidator = selectedValidator ?: yield.preferredValidators.first(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 462c5d22c0..27ba05eb91 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -42,6 +43,7 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.RewardBlockType import com.tangem.features.staking.impl.R @@ -171,7 +173,8 @@ private fun LazyListScope.activeStakingBlock( currentIndex = index + 1, lastIndex = state.yieldBalance.balances.lastIndex + 1, addDefaultPadding = false, - ), + ) + .testTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK), ) } } @@ -189,7 +192,9 @@ private fun BannerBlock(onClick: () -> Unit) { ), ) { Image( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(StakingDetailsScreenTestTags.BANNER_IMAGE), contentScale = ContentScale.FillWidth, painter = painterResource(R.drawable.img_staking_banner), contentDescription = null, @@ -197,7 +202,8 @@ private fun BannerBlock(onClick: () -> Unit) { Text( modifier = Modifier .align(Alignment.CenterStart) - .padding(TangemTheme.dimens.spacing16), + .padding(TangemTheme.dimens.spacing16) + .testTag(StakingDetailsScreenTestTags.BANNER_TEXT), text = buildAnnotatedString { withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) { append(stringResourceSafe(R.string.staking_details_banner_text)) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 9cc528d859..e1a7c81bd2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig @@ -20,6 +21,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep @@ -44,7 +46,8 @@ internal fun StakingScreen(uiState: StakingUiState) { .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() .imePadding() - .systemBarsPadding(), + .systemBarsPadding() + .testTag(StakingSendScreenTestTags.SCREEN_CONTAINER), horizontalAlignment = Alignment.CenterHorizontally, ) { StakingAppBar( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt index c7e67f5415..b09a9ab029 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt @@ -2,11 +2,14 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.features.staking.impl.R private const val TERMS_OF_USE_KEY = "termsOfUse" @@ -58,5 +61,6 @@ internal fun StakingTosText(onTextClick: (String) -> Unit) { onTextClick(PRIVACY_POLICY_URL) } }, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.TOS_TEXT), ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index bce18ef1ec..7efc5b644c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -9,6 +9,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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -19,6 +20,7 @@ import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto @@ -26,9 +28,10 @@ import com.tangem.core.ui.format.bigdecimal.fee 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.utils.BigDecimalFormatter +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.utils.StringsSigns.DASH_SIGN import java.math.BigDecimal @Composable @@ -38,7 +41,8 @@ internal fun StakingFeeBlock(feeState: FeeState) { .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK), ) { Text( text = stringResourceSafe(R.string.common_network_fee_title), @@ -51,7 +55,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = stringReference( feeAmount?.value.format { @@ -75,7 +79,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Loading -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), @@ -85,7 +89,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Error -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), @@ -126,7 +130,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { ) { if (it == FeeState.Error) { Text( - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 6e2d13e5e0..7182fc7bf4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder @@ -35,7 +37,8 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, - ), + ) + .testTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK), ) { InputRowImageInfo( title = resourceReference(R.string.staking_validator), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index f5738cbd5e..c014e539e7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -57,6 +57,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } else { @@ -68,6 +69,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index b9d220ba0b..adfcc37a91 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.Differ import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.StringsSigns import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -26,12 +27,20 @@ internal class SwapAmountSetQuotesTransformer( override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + val isSingleProvider = quotes.filter { + it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || + (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + }.isSingleItem() + val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { prevState.selectedQuote } else { - (bestQuote as? SwapQuoteUM.Content)?.copy(diffPercent = DifferencePercent.Best) ?: bestQuote + (bestQuote as? SwapQuoteUM.Content)?.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) ?: bestQuote } val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( @@ -47,16 +56,23 @@ internal class SwapAmountSetQuotesTransformer( return updatedState.copy( isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote), + swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), ) } - private fun getQuotesWithDiff(sortedQuotes: List, bestQuote: SwapQuoteUM): ImmutableList { + private fun getQuotesWithDiff( + sortedQuotes: List, + bestQuote: SwapQuoteUM, + isSingleProvider: Boolean, + ): ImmutableList { return sortedQuotes.sortedWith(SwapQuotesComparator) .map { quote -> if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy(diffPercent = DifferencePercent.Best) + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { // current / selected - 1 val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index b788f33190..f7129e04bc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.ui.extensions.TextReference @@ -34,6 +36,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -63,34 +66,11 @@ internal fun SwapAmountBlockContent( ), ) { val (from, to, separator, provider) = createRefs() - AmountBlockV2( - amountState = amountUM.primaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(from) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) - }, - ) - AmountBlockV2( - amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( - title = resourceReference(R.string.send_with_swap_recipient_amount_title), - availableBalance = TextReference.EMPTY, - ) ?: amountUM.secondaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(to) { - top.linkTo(from.bottom, 8.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick) - }, + SwapAmountBlock( + amountUM = amountUM, + fromAmountRef = from, + toAmountRef = to, + onInfoClick = onInfoClick, ) SwapAmountDivider( modifier = Modifier.constrainAs(separator) { @@ -104,6 +84,7 @@ internal fun SwapAmountBlockContent( val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( isBestRate = isBestRate, + isSingleProvider = quoteContent?.isSingleProvider == true, showBestRateAnimation = amountUM.showBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, @@ -120,29 +101,88 @@ internal fun SwapAmountBlockContent( } @Composable -private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) { +private fun ConstraintLayoutScope.SwapAmountBlock( + amountUM: SwapAmountUM.Content, + fromAmountRef: ConstrainedLayoutReference, + toAmountRef: ConstrainedLayoutReference, + onInfoClick: () -> Unit, +) { + AmountBlockV2( + amountState = amountUM.primaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(fromAmountRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.primaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) + AmountBlockV2( + amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( + title = resourceReference(R.string.send_with_swap_recipient_amount_title), + availableBalance = TextReference.EMPTY, + availableBalanceShort = TextReference.EMPTY, + ) ?: amountUM.secondaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(toAmountRef) { + top.linkTo(fromAmountRef.bottom, 8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.secondaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) +} + +@Composable +private fun SwapPriceImpact( + amountFieldUM: SwapAmountFieldUM, + selectedAmountType: SwapAmountType, + onInfoClick: () -> Unit, +) { + if (amountFieldUM.amountType == selectedAmountType) return + val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact + val iconColor = if (priceImpact != null) { + TangemTheme.colors.icon.attention + } else { + TangemTheme.colors.icon.informative + } + if (priceImpact != null) { Text( text = priceImpact.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.attention, ) - Icon( - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_information_24), - ), - tint = TangemTheme.colors.icon.attention, - contentDescription = null, - modifier = Modifier - .size(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = onInfoClick, - ), - ) } + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_information_24), + ), + tint = iconColor, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onInfoClick, + ), + ) } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 627af48d84..02419e0cd6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -69,6 +69,7 @@ internal data object SwapAmountContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) val emptyState = SwapAmountUM.Content( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 9772ead6c8..77f8f0cbef 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -5,14 +5,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -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.alpha -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -65,7 +63,7 @@ internal fun SwapChooseProviderContent( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.padding(horizontal = 13.dp), + modifier = modifier.padding(horizontal = 12.dp), ) { Text( text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint), @@ -89,7 +87,6 @@ internal fun SwapChooseProviderContent( SwapProviderItem( state = provider.swapProviderState, modifier = Modifier - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = provider.swapProviderState.isSelected) .clickable( enabled = provider.quote !is SwapQuoteUM.Error, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 6751315374..a853552187 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.delay @Composable fun SwapChooseProviderContent( expressProvider: ExpressProvider?, + isSingleProvider: Boolean, isBestRate: Boolean, showBestRateAnimation: Boolean, onClick: () -> Unit, @@ -62,6 +63,7 @@ fun SwapChooseProviderContent( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, + enabled = !isSingleProvider, ), ) { HorizontalDivider( @@ -88,6 +90,7 @@ fun SwapChooseProviderContent( ProviderInfo( expressProvider = expressProvider, isBestRate = isBestRate, + isSingleProvider = isSingleProvider, showBestRateAnimation = showBestRateAnimation, onFinishAnimation = onFinishAnimation, ) @@ -125,6 +128,7 @@ private fun FcaProviderWarning(modifier: Modifier = Modifier) { private fun ProviderInfo( expressProvider: ExpressProvider?, isBestRate: Boolean, + isSingleProvider: Boolean, showBestRateAnimation: Boolean, onFinishAnimation: () -> Unit, modifier: Modifier = Modifier, @@ -166,6 +170,7 @@ private fun ProviderInfo( start.linkTo(imageRef.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) + end.linkTo(iconRef.start, goneMargin = 12.dp) }, ) Icon( @@ -180,12 +185,13 @@ private fun ProviderInfo( start.linkTo(nameRef.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) - end.linkTo(parent.end, 12.dp) + end.linkTo(parent.end, margin = 12.dp) + visibility = if (isSingleProvider) Visibility.Gone else Visibility.Visible }, ) BestRateBadge( showBestRateAnimation = showBestRateAnimation, - isBestRate = isBestRate, + isBestRate = isBestRate && !isSingleProvider, ref = imageRef, onFinishAnimation = onFinishAnimation, ) @@ -325,6 +331,7 @@ private fun SwapChooseProviderContent_Preview() { modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) { SwapChooseProviderContent( + isSingleProvider = false, isBestRate = true, showBestRateAnimation = true, expressProvider = ExpressProvider( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 0975e02bdf..238cf49cb3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -39,6 +39,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) private val quote2 = SwapQuoteUM.Content( @@ -47,6 +48,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("13.12"), rate = stringReference("1 USD ≈ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSingleProvider = false, ) val state = SwapChooseProviderBottomSheetContent( @@ -96,13 +98,13 @@ internal object SwapChooseProviderContentPreview { ), quote = quote2, swapProviderState = SwapProviderState.Content( - name = provider1.name, - type = provider1.type.typeName, + name = provider2.name, + type = provider2.type.typeName, iconUrl = "", subtitle = stringReference("1800 POL"), additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, - isSelected = true, + isSelected = false, ), ), ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index ff661ba3ee..c8b7e6f038 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -34,6 +34,7 @@ internal sealed class SwapQuoteUM { val quoteAmount: BigDecimal, val quoteAmountValue: TextReference, val diffPercent: DifferencePercent, + val isSingleProvider: Boolean, val rate: TextReference, ) : SwapQuoteUM() { sealed class DifferencePercent { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index ed82f2f734..bc241e0ec0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -160,7 +160,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, callback = model, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index a5aa42ce31..cebaaba9dd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -406,6 +406,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( it.second is SendWithSwapRoute.Confirm }.onEach { (state, _) -> val confirmUM = state.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isTransactionInProcess params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( @@ -416,6 +417,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( primaryButton = NavigationButton( textReference = resourceReference(R.string.common_send), iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isHapticClick = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, onClick = { when (confirmUM) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 2ad404315d..6e5406b9e6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -1,7 +1,6 @@ package com.tangem.features.swap.v2.impl.sendviaswap.success.ui import android.content.res.Configuration -import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -10,14 +9,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -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.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.transaction.Fee @@ -25,9 +19,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.InputRowBestRate @@ -41,7 +35,6 @@ 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.utils.DateTimeFormatters -import com.tangem.core.ui.utils.singleEvent import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType @@ -64,8 +57,6 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -private const val GRADIENT_ALPHA = 0.3f - @Composable internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { if (sendWithSwapUM.navigationUM !is NavigationUM.Content) return @@ -112,24 +103,15 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { FeeBlock(feeSelectorUM = feeSelectorUM) Spacer(Modifier.height(60.dp)) } - DoneButtons( - pairButtonsUM = sendWithSwapUM.navigationUM.secondaryPairButtonsUM, + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendWithSwapUM.navigationUM, modifier = Modifier .align(Alignment.BottomCenter) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - TangemTheme.colors.background.tertiary.copy(GRADIENT_ALPHA), - TangemTheme.colors.background.tertiary, - ), - ), - ) .padding( - top = 24.dp, - bottom = 12.dp, start = 16.dp, end = 16.dp, + bottom = 16.dp, ), ) } @@ -220,16 +202,17 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) { .padding(TangemTheme.dimens.spacing12), ) { Text( - text = stringResourceSafe(com.tangem.common.ui.R.string.common_network_fee_title), + text = stringResourceSafe(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { - val feeAmount = feeSelectorUM.selectedFeeItem.fee.amount + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount SelectorRowItem( - titleRes = com.tangem.common.ui.R.string.common_fee_selector_option_market, - iconRes = com.tangem.common.ui.R.drawable.ic_bird_24, + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, preDot = stringReference( feeAmount.value.format { crypto( @@ -289,46 +272,6 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m } } -// TODO remove [REDACTED_TASK_KEY] -@Composable -private fun DoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = requireNotNull(leftButton.iconRes), - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = requireNotNull(rightButton.iconRes), - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} - // region Preview @Suppress("LongMethod") @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index e9d03433d4..3075a24638 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -13,11 +13,9 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.plus import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton -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.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -57,26 +55,14 @@ internal fun SendWithSwapContent( ) { it.instance.Content(Modifier.weight(1f)) } - // TODO refactor [REDACTED_TASK_KEY] - val primaryButton = navigationUM.primaryButton - Row( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - ) { - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = primaryButton.onClick, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, + if (stackState.active.configuration != SendWithSwapRoute.Success) { + NavigationPrimaryButton( + navigationUM.primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), ) } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index a23bc00f4b..96a200e419 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -25,12 +25,12 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError @@ -51,10 +51,11 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListManager: UserWalletsListManager, + private val userWalletsStore: UserWalletsStore, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, + private val rampStateManager: RampStateManager, moshi: Moshi, excludedBlockchains: ExcludedBlockchains, ) : SwapRepository { @@ -138,7 +139,8 @@ internal class DefaultSwapRepository( val currenciesList = currencyList .filter { val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it) - requirements !is AssetRequirementsCondition.RequiredTrustline + val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) + isAvailableForSwap } .map { leastTokenInfoConverter.convert(it) } @@ -409,7 +411,7 @@ internal class DefaultSwapRepository( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - userWallet = requireNotNull(userWalletsListManager.selectedUserWalletSync), + userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull), ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 679015d755..4aada993a6 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,8 +8,9 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.converters.ErrorsDataConverter @@ -33,22 +34,24 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, + userWalletsStore: UserWalletsStore, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, appPreferencesStore: AppPreferencesStore, + rampStateManager: RampStateManager, ): SwapRepository { return DefaultSwapRepository( tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - userWalletsListManager = userWalletsListManager, + userWalletsStore = userWalletsStore, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, excludedBlockchains = excludedBlockchains, appPreferencesStore = appPreferencesStore, + rampStateManager = rampStateManager, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index abfa93bd3a..de44a22010 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -17,6 +17,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -31,7 +32,6 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -82,6 +82,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase, private val amountFormatter: AmountFormatter, + private val rampStateManager: RampStateManager, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -179,12 +180,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): List? { val requirements = getAssetRequirementsUseCase.invoke(userWalletId, cryptoCurrencyStatuses.currency).getOrNull() + val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) return swapPairsLeastList.firstNotNullOfOrNull { val listTokenInfo = tokenInfoForAvailable(it) if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && - requirements !is AssetRequirementsCondition.RequiredTrustline + isAvailableForSwap ) { it.providers } else { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d31fdcffe5..86245f0585 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -3,8 +3,8 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -111,10 +111,11 @@ class TokensDataConverter( } private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = cryptoCurrencyStatus.value.fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return cryptoCurrencyStatus.value.fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 1932d52634..3946ea9358 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -14,8 +14,8 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -110,7 +110,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), @@ -122,7 +122,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { } FeeType.PRIORITY -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_fast, + title = resourceReference(R.string.common_fee_selector_option_fast), iconRes = R.drawable.ic_hare_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 9f250ee99b..0de047350a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -13,8 +13,8 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals 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.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency @@ -1273,7 +1273,12 @@ internal class StateBuilder( private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) + return amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { diff --git a/features/tangempay/details/api/.gitignore b/features/tangempay/details/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/api/build.gradle.kts b/features/tangempay/details/api/build.gradle.kts new file mode 100644 index 0000000000..77acdd5c22 --- /dev/null +++ b/features/tangempay/details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt new file mode 100644 index 0000000000..393e589bce --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tangempay + +interface TangemPayFeatureToggles { + val isTangemPayEnabled: Boolean +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt new file mode 100644 index 0000000000..64e11cecbe --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface TangemPayDetailsComponent : ComposableContentComponent { + @Suppress("EmptyDefaultConstructor") // Will add params in Next PRs + class Params() + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/.gitignore b/features/tangempay/details/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts new file mode 100644 index 0000000000..4f9fb1c110 --- /dev/null +++ b/features/tangempay/details/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt new file mode 100644 index 0000000000..a51c11a3bc --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTangemPayFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TangemPayFeatureToggles { + override val isTangemPayEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt new file mode 100644 index 0000000000..2c55cb6a3a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.decompose.context.AppComponentContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: TangemPayDetailsComponent.Params, +) : AppComponentContext by appComponentContext, TangemPayDetailsComponent { + + @Composable + override fun Content(modifier: Modifier) { + Box(modifier.fillMaxSize().background(Color.Red)) + // TODO("[REDACTED_JIRA]") + } + + @AssistedFactory + interface Factory : TangemPayDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: TangemPayDetailsComponent.Params, + ): DefaultTangemPayDetailsComponent + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt new file mode 100644 index 0000000000..4cd92fc806 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.components.DefaultTangemPayDetailsComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayDetailsFeatureModule { + + @Binds + @Singleton + fun bindTangemPayDetailsComponentFactory( + factory: DefaultTangemPayDetailsComponent.Factory, + ): TangemPayDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt new file mode 100644 index 0000000000..a6ea142d28 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.tangempay.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemPayDetailsModule { + + @Provides + @Singleton + fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { + return DefaultTangemPayFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/tangempay/main/api/.gitignore b/features/tangempay/main/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts new file mode 100644 index 0000000000..15fb515b8b --- /dev/null +++ b/features/tangempay/main/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/main/impl/.gitignore b/features/tangempay/main/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts new file mode 100644 index 0000000000..eb442c8f69 --- /dev/null +++ b/features/tangempay/main/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index ae100c11a3..d1110e97fa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -20,11 +20,11 @@ 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.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.toImmutableList @Suppress("DestructuringDeclarationWithTooManyEntries") @@ -124,7 +124,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -154,7 +154,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index cd79642bbc..d077ce9d52 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -17,6 +18,7 @@ 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.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.features.tokendetails.impl.R @@ -30,7 +32,9 @@ internal fun StakingBalanceBlock( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.STAKING_BLOCK), ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), @@ -47,16 +51,19 @@ internal fun StakingBalanceBlock( text = state.fiatValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT), ) Text( text = StringsSigns.DOT, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_DOT), ) Text( text = state.cryptoValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT), ) } if (state.rewardValue != TextReference.EMPTY) { @@ -64,6 +71,7 @@ internal fun StakingBalanceBlock( text = state.rewardValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_REWARD_VALUE), ) } } @@ -71,6 +79,7 @@ internal fun StakingBalanceBlock( painter = painterResource(id = R.drawable.ic_chevron_right_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON), ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index 807664c039..39bfbef5ec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -13,6 +13,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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -23,6 +24,7 @@ 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.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock @@ -77,7 +79,9 @@ internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean, @Composable private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifier: Modifier = Modifier) { Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK), ) { Row { val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { @@ -87,7 +91,8 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi modifier = Modifier .size(TangemTheme.dimens.size20) .clip(TangemTheme.shapes.roundedCorners8) - .align(Alignment.CenterVertically), + .align(Alignment.CenterVertically) + .testTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON), icon = state.iconState, alpha = alpha, colorFilter = colorFilter, @@ -98,6 +103,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi text = state.titleText.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) @@ -106,6 +112,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi text = state.subtitleText.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size8)) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt index 3cadad2268..39bd16ae56 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -9,4 +9,5 @@ internal data class WalletSettingsUM( val items: PersistentList, val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, + val isWalletBackedUp: Boolean = true, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 22dd035507..f9d0b633b2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -15,10 +15,16 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO @@ -86,9 +92,29 @@ internal class WalletSettingsModel @Inject constructor( items = persistentListOf(), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, + isWalletBackedUp = true, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + closeBs() + } + } + } + init { combine( getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), @@ -97,6 +123,10 @@ internal class WalletSettingsModel @Inject constructor( ) { maybeWallet, nftEnabled, notificationsEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + val isWalletBackedUp = when (wallet) { + is UserWallet.Hot -> wallet.backedUp + is UserWallet.Cold -> true + } val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && !getIsHuaweiDeviceWithoutGoogleServicesUseCase() state.update { value -> @@ -110,6 +140,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), ), + isWalletBackedUp = isWalletBackedUp, ) } } @@ -330,6 +361,10 @@ internal class WalletSettingsModel @Inject constructor( } private fun onAccessCodeClick() { - router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + if (!state.value.isWalletBackedUp) { + messageSender.send(makeBackupAtFirstAlertBS) + } else { + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + } } } \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt index 3f30b7eebe..674541796f 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt @@ -14,6 +14,7 @@ interface UserWalletsFetcher { fun create( messageSender: UiMessageSender, onlyMultiCurrency: Boolean, + authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): UserWalletsFetcher } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index a71f5825e3..a8fffc2ea0 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -114,6 +114,7 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.kyc.api) implementation(projects.features.tokenRecieve.api) /** Common modules */ diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 94c51ca420..8b75c0188c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -77,6 +78,7 @@ internal class WalletComponent @AssistedInject constructor( params = PushNotificationsParams( isBottomSheet = true, modelCallbacks = model.askForPushNotificationsModelCallbacks, + source = AppRoute.PushNotification.Source.Main, ), ) is WalletDialogConfig.TokenReceive -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index e62305cd29..14324845f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase @@ -84,7 +85,7 @@ internal class WalletModel @Inject constructor( private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val notificationsFeatureToggles: NotificationsFeatureToggles, - private val getIsBiometryIsEnabledUseCase: GetIsBiometricsEnabledUseCase, + private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -212,9 +213,15 @@ internal class WalletModel @Inject constructor( modelScope.launch { val shouldAskPermission = shouldAskPermissionUseCase(PUSH_PERMISSION) val afterUpdate = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() - val isBiometricsEnabled = getIsBiometryIsEnabledUseCase() + val isBiometricsEnabled = shouldSaveUserWalletsSyncUseCase() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() val shouldShowBottomSheet = shouldAskPermission || afterUpdate + Timber.d( + "push BS afterUpdate: $afterUpdate," + + "shouldAskPermission $shouldAskPermission," + + "isBiometricsEnabled $isBiometricsEnabled," + + "isHuaweiDevice $isHuaweiDevice", + ) if (!isBiometricsEnabled) return@launch if (isHuaweiDevice) return@launch if (!shouldShowBottomSheet) return@launch @@ -383,9 +390,11 @@ internal class WalletModel @Inject constructor( val otherWallets = action.wallets.minus(action.selectedWallet) if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - otherWallets.onEach { userWallet -> - modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } - } + otherWallets + .filterNot(UserWallet::isLocked) + .onEach { userWallet -> + modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } } if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { @@ -500,6 +509,10 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, coroutineScope = modelScope, ) + + action.unlockedWallets.onEach { userWallet -> + modelScope.launch { fetchWalletContent(userWallet = userWallet) } + } } private fun demonstrateWalletsScrollPreview(direction: Direction) { @@ -541,6 +554,8 @@ internal class WalletModel @Inject constructor( private suspend fun fetchWalletContent(userWallet: UserWallet) { if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + if (userWallet.isLocked) return + /* * Updating the balance of the current wallet is an essential part of InitializationWallets, * so the coroutine is launched in the current context diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index a7454d94db..55fd1893e3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -5,13 +5,14 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -87,7 +88,7 @@ internal class WalletClickIntents @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled && !it.isLocked) { launch { walletContentFetcher(userWalletId = it.walletId) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt index 1657e84545..3f365ebc8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -2,29 +2,44 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import timber.log.Timber class WalletNameMigrationUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, private val walletNamesMigrationRepository: WalletNamesMigrationRepository, ) { suspend operator fun invoke() { - val wallets = userWalletsListManager.userWalletsSync - if (walletNamesMigrationRepository.isMigrationDone()) { return } - val existingNames: MutableSet = mutableSetOf() - wallets.indices.forEach { i -> - val defaultName = wallets[i].name - val suggestedWalletName = suggestedWalletName(defaultName, existingNames) - if (defaultName != suggestedWalletName) { - userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + if (useNewListRepository) { + val wallets = userWalletsListRepository.userWalletsSync() + val existingNames: MutableSet = mutableSetOf() + wallets.forEach { + val defaultName = it.name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) + } + Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) + } + } else { + val wallets = userWalletsListManager.userWalletsSync + val existingNames: MutableSet = mutableSetOf() + wallets.indices.forEach { i -> + val defaultName = wallets[i].name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + } + Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } - Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 8dadf5a69e..a2dc7210ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -4,11 +4,13 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( @@ -47,17 +49,18 @@ internal class SingleWalletMarketPriceConverter( } private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { - val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatRate = status.fiatRate ?: return DASH_SIGN - return BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = fiatRate, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return fiatRate.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).uncapped() + } } private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { - val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val priceChange = status.priceChange ?: return DASH_SIGN return priceChange.format { percent() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index 99b787897e..f1ea722bd8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -2,13 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.capitalize 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.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxDetails -import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList import org.joda.time.DateTimeZone @@ -71,11 +71,12 @@ internal class VisaTxDetailsBottomSheetConverter( } private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount, - fiatCurrencyCode = fiatCurrency.currencyCode, - fiatCurrencySymbol = fiatCurrency.symbol, - ) + return amount.format { + fiat( + fiatCurrencyCode = fiatCurrency.currencyCode, + fiatCurrencySymbol = fiatCurrency.symbol, + ) + } } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 62d00e4c3c..0ec063a2f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -4,13 +4,13 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference 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.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxHistoryItem -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.impl.R import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -29,11 +29,12 @@ internal class VisaTxHistoryItemStateConverter( txHash = value.id, amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }, // Show tx fiat amount instead of tx time - time = BigDecimalFormatter.formatFiatAmount( - fiatAmount = value.fiatAmount, - fiatCurrencyCode = value.fiatCurrency.currencyCode, - fiatCurrencySymbol = value.fiatCurrency.symbol, - ), + time = value.fiatAmount.format { + fiat( + fiatCurrencyCode = value.fiatCurrency.currencyCode, + fiatCurrencySymbol = value.fiatCurrency.symbol, + ) + }, status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.INCOMING, iconRes = R.drawable.ic_arrow_up_24, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index ce71cf24a6..7a3b9accba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -43,7 +43,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @Assisted private val onWalletClick: (UserWalletId) -> Unit, @Assisted private val messageSender: UiMessageSender, - @Assisted private val onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, + @Assisted("authMode") private val authMode: Boolean, private val getCardImageUseCase: GetCardImageUseCase, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { @@ -54,7 +55,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override val userWallets: Flow> = walletsFlow.transformLatest { wallets -> - val uiModels = UserWalletItemUMConverter(onClick = onWalletClick).convertList(wallets) + val uiModels = UserWalletItemUMConverter( + onClick = onWalletClick, + authMode = authMode, + ).convertList(wallets) .toImmutableList() emit(uiModels) @@ -132,6 +136,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, artwork = artworks[userWallet.walletId], + authMode = authMode, ) .convert(userWallet) } @@ -149,7 +154,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( interface Factory : UserWalletsFetcher.Factory { override fun create( messageSender: UiMessageSender, - onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean, + @Assisted("authMode") authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): DefaultUserWalletsFetcher } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt index 6fd6fde73b..fd682083d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt @@ -38,5 +38,23 @@ internal class AlertsComponent( @Serializable data class WcDisconnected(override val onDismiss: () -> Unit) : AlertType() + + @Serializable + data class TangemUnsupportedNetwork( + val network: String, + override val onDismiss: () -> Unit, + ) : AlertType() + + @Serializable + data class RequiredAddNetwork( + val network: String, + override val onDismiss: () -> Unit, + ) : AlertType() + + @Serializable + data class RequiredReconnectWithNetwork( + val network: String, + override val onDismiss: () -> Unit, + ) : AlertType() } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index a171a0f38a..afa23ea711 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -57,7 +57,7 @@ internal class WcPairComponent( private fun onChildBack() { when (val config = contentStack.value.active.configuration) { is WcAppInfoRoutes.AppInfo -> dismiss() - is Alert -> when (config.type) { + is Alert -> when (config.alertType) { is Alert.Type.UnsupportedDApp, is Alert.Type.UnsupportedNetwork, -> dismiss() @@ -85,7 +85,7 @@ internal class WcPairComponent( ) is Alert -> AlertsComponentV2( appComponentContext = appComponentContext, - messageUM = createBottomSheetMessageUM(config.type), + messageUM = createBottomSheetMessageUM(config.alertType), ) is WcAppInfoRoutes.SelectNetworks -> WcSelectNetworksComponent( appComponentContext = appComponentContext, @@ -114,12 +114,14 @@ internal class WcPairComponent( return when (alertType) { is Alert.Type.Verified -> WcAlertsFactory.createVerifiedDomainAlert(alertType.appName) is Alert.Type.UnknownDomain -> WcAlertsFactory.createUnknownDomainAlert(model::connectFromAlert) + is Alert.Type.InvalidDomain -> WcAlertsFactory.createInvalidDomainAlert(model::errorAlertOnDismiss) is Alert.Type.UnsafeDomain -> WcAlertsFactory.createUnsafeDomainAlert(model::connectFromAlert) is Alert.Type.UnsupportedDApp -> WcAlertsFactory.createUnsupportedDomainAlert(alertType.appName, model::errorAlertOnDismiss) is Alert.Type.UnsupportedNetwork -> WcAlertsFactory.createUnsupportedChainAlert(alertType.appName, model::errorAlertOnDismiss) is Alert.Type.UriAlreadyUsed -> WcAlertsFactory.createUriAlreadyUsedAlert(model::errorAlertOnDismiss) + is Alert.Type.TimeoutException -> WcAlertsFactory.createTimeoutExceptionAlert(model::errorAlertOnDismiss) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 9a63c9baad..fe64931b39 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency @@ -69,8 +70,9 @@ internal class WcPairModel @Inject constructor( val stackNavigation = StackNavigation() - private val selectedUserWalletFlow = + private val selectedUserWalletFlow: MutableStateFlow by lazy { MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) + } private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() private var additionallyEnabledNetworks = setOf() @@ -214,15 +216,11 @@ internal class WcPairModel @Inject constructor( private fun processError(error: WcPairError) { val alert = when (error) { - is WcPairError.UnsupportedDApp -> { - WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName) - } - is WcPairError.UnsupportedBlockchains -> { - WcAppInfoRoutes.Alert.Type.UnsupportedNetwork(error.appName) - } - is WcPairError.UriAlreadyUsed -> { - WcAppInfoRoutes.Alert.Type.UriAlreadyUsed - } + is WcPairError.InvalidDomainURL -> WcAppInfoRoutes.Alert.Type.InvalidDomain + is WcPairError.UnsupportedDApp -> WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName) + is WcPairError.UnsupportedBlockchains -> WcAppInfoRoutes.Alert.Type.UnsupportedNetwork(error.appName) + is WcPairError.UriAlreadyUsed -> WcAppInfoRoutes.Alert.Type.UriAlreadyUsed + is WcPairError.TimeoutException -> WcAppInfoRoutes.Alert.Type.TimeoutException else -> { messageSender.send(ToastMessage(message = stringReference(error.message))) router.pop() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index cf5b226353..784e0457ed 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -26,15 +26,17 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { ) : WcAppInfoRoutes() @Serializable - data class Alert(val type: Type) : WcAppInfoRoutes() { + data class Alert(val alertType: Type) : WcAppInfoRoutes() { @Serializable sealed class Type { data class Verified(val appName: String) : Type() data object UnknownDomain : Type() data object UnsafeDomain : Type() + data object InvalidDomain : Type() data class UnsupportedDApp(val appName: String) : Type() data class UnsupportedNetwork(val appName: String) : Type() data object UriAlreadyUsed : Type() + data object TimeoutException : Type() } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 42b336932f..30c6d10c14 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -18,8 +18,10 @@ import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.connections.components.AlertsComponent +import com.tangem.features.walletconnect.connections.components.AlertsComponent.AlertType.* import com.tangem.features.walletconnect.connections.components.WcPairComponent import com.tangem.features.walletconnect.transaction.components.chain.WcAddNetworkContainerComponent +import com.tangem.features.walletconnect.transaction.components.chain.WcSwitchNetworkComponent import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.send.WcSendTransactionContainerComponent import com.tangem.features.walletconnect.transaction.components.sign.WcSignTransactionContainerComponent @@ -71,6 +73,10 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), ) + is WcInnerRoute.SwitchNetwork -> WcSwitchNetworkComponent( + appComponentContext = childContext, + params = WcTransactionModelParams(config.rawRequest), + ) is WcInnerRoute.Send -> WcSendTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), @@ -88,13 +94,31 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( is WcInnerRoute.UnsupportedMethodAlert -> AlertsComponent( childContext, AlertsComponent.Params( - alertType = AlertsComponent.AlertType.UnsupportedMethod { model.innerRouter.pop() }, + alertType = UnsupportedMethod { model.innerRouter.pop() }, ), ) is WcInnerRoute.WcDappDisconnected -> AlertsComponent( childContext, AlertsComponent.Params( - alertType = AlertsComponent.AlertType.WcDisconnected { model.innerRouter.pop() }, + alertType = WcDisconnected { model.innerRouter.pop() }, + ), + ) + is WcInnerRoute.TangemUnsupportedNetwork -> AlertsComponent( + childContext, + AlertsComponent.Params( + alertType = TangemUnsupportedNetwork(config.networkName) { model.innerRouter.pop() }, + ), + ) + is WcInnerRoute.RequiredAddNetwork -> AlertsComponent( + childContext, + AlertsComponent.Params( + alertType = RequiredAddNetwork(config.networkName) { model.innerRouter.pop() }, + ), + ) + is WcInnerRoute.RequiredReconnectWithNetwork -> AlertsComponent( + childContext, + AlertsComponent.Params( + alertType = RequiredReconnectWithNetwork(config.networkName) { model.innerRouter.pop() }, ), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt index 4cabfc0547..bc9f386e88 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt @@ -22,6 +22,9 @@ internal sealed interface WcInnerRoute : Route { @Serializable data class AddNetwork(override val rawRequest: WcSdkSessionRequest) : Method + @Serializable + data class SwitchNetwork(override val rawRequest: WcSdkSessionRequest) : Method + @Serializable data class Pair(val request: WcPairRequest) : WcInnerRoute @@ -30,4 +33,13 @@ internal sealed interface WcInnerRoute : Route { @Serializable data object WcDappDisconnected : WcInnerRoute + + @Serializable + data class TangemUnsupportedNetwork(val networkName: String) : WcInnerRoute + + @Serializable + data class RequiredAddNetwork(val networkName: String) : WcInnerRoute + + @Serializable + data class RequiredReconnectWithNetwork(val networkName: String) : WcInnerRoute } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt index 3c320faab7..5524f954e3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt @@ -51,6 +51,8 @@ internal class WcRoutingModel @Inject constructor( -> WcInnerRoute.SignMessage(rawRequest) WcEthMethodName.AddEthereumChain, -> WcInnerRoute.AddNetwork(rawRequest) + WcEthMethodName.SwitchEthereumChain, + -> WcInnerRoute.SwitchNetwork(rawRequest) WcEthMethodName.SignTransaction, WcEthMethodName.SendTransaction, WcSolanaMethodName.SignTransaction, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt index 7c93a184f5..30370ab112 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt @@ -87,7 +87,11 @@ private fun ButtonsContainer(alert: AlertsComponent.AlertType, modifier: Modifie ) { val buttonsModifier = Modifier.fillMaxWidth() when (alert) { - is AlertsComponent.AlertType.UnsupportedMethod -> SecondaryButton( + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> SecondaryButton( modifier = buttonsModifier, onClick = alert.onDismiss, text = stringResourceSafe(R.string.balance_hidden_got_it_button), @@ -105,16 +109,28 @@ private fun ButtonsContainer(alert: AlertsComponent.AlertType, modifier: Modifie private fun AlertIcon(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { val color = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> TangemTheme.colors.icon.informative - is AlertsComponent.AlertType.UnsupportedMethod -> TangemTheme.colors.icon.attention + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> TangemTheme.colors.icon.attention } @DrawableRes val drawableId = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> R.drawable.ic_wallet_connect_24 - is AlertsComponent.AlertType.UnsupportedMethod -> R.drawable.img_attention_20 + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> R.drawable.img_attention_20 } val iconTint = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> color - is AlertsComponent.AlertType.UnsupportedMethod -> Color.Unspecified + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> Color.Unspecified } Box( modifier = modifier @@ -138,6 +154,9 @@ private fun AlertContentTitle(alert: AlertsComponent.AlertType, modifier: Modifi @StringRes val titleRes: Int = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> R.string.wc_alert_session_disconnected_title is AlertsComponent.AlertType.UnsupportedMethod -> R.string.wc_alert_unsupported_method_title + is AlertsComponent.AlertType.RequiredAddNetwork -> R.string.wc_alert_add_network_to_portfolio_title + is AlertsComponent.AlertType.RequiredReconnectWithNetwork -> R.string.wc_alert_network_not_connected_title + is AlertsComponent.AlertType.TangemUnsupportedNetwork -> R.string.wc_alert_unsupported_network_title } Text( modifier = modifier, @@ -157,6 +176,18 @@ private fun AlertContentDescription(alert: AlertsComponent.AlertType, modifier: is AlertsComponent.AlertType.UnsupportedMethod -> stringResourceSafe( R.string.wc_alert_unsupported_method_description, ) + is AlertsComponent.AlertType.RequiredAddNetwork -> stringResourceSafe( + R.string.wc_alert_add_network_to_portfolio_description, + alert.network, + ) + is AlertsComponent.AlertType.RequiredReconnectWithNetwork -> stringResourceSafe( + R.string.wc_alert_network_not_connected_description, + alert.network, + ) + is AlertsComponent.AlertType.TangemUnsupportedNetwork -> stringResourceSafe( + R.string.wc_alert_unsupported_network_description, + alert.network, + ) } Text( modifier = modifier, @@ -188,5 +219,8 @@ private class AlertTypesProvider : CollectionPreviewParameterProvider Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_wallet_connect_24) { + type = Type.Informative + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.wc_errors_invalid_domain_title) + body = resourceReference(R.string.wc_errors_invalid_domain_subtitle) + } + primaryButton { + text = resourceReference(R.string.common_got_it) + onClick { onDismiss() } + } + onDismissRequest = onDismiss + } + } + fun createUnsafeDomainAlert(activeButtonOnClick: (() -> Unit)? = null): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { @@ -109,6 +127,23 @@ internal object WcAlertsFactory { } } + fun createTimeoutExceptionAlert(onDismiss: () -> Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_wallet_connect_24) { + type = Type.Informative + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.wc_alert_request_timeout_title) + body = resourceReference(R.string.wc_alert_request_timeout_description) + } + primaryButton { + text = resourceReference(R.string.common_got_it) + onClick { onDismiss() } + } + } + } + fun createUnsupportedChainAlert(appName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index bcfd5129b1..727e85c485 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -29,6 +29,7 @@ internal class WcUserWalletsFetcher( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = true, + authMode = false, onWalletClick = { onWalletSelected(it) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt index 623136bf30..f40742eb25 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt @@ -7,6 +7,7 @@ import com.tangem.features.walletconnect.connections.routing.WcRoutingModel import com.tangem.features.walletconnect.transaction.model.WcAddNetworkModel import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel +import com.tangem.features.walletconnect.transaction.model.WcSwitchNetworkModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -57,6 +58,11 @@ internal interface WalletConnectModelModule { @ClassKey(WcAddNetworkModel::class) fun bindWcAddNetworkModel(model: WcAddNetworkModel): Model + @Binds + @IntoMap + @ClassKey(WcSwitchNetworkModel::class) + fun bindWcSwitchNetworkModel(model: WcSwitchNetworkModel): Model + @Binds @IntoMap @ClassKey(WcSendTransactionModel::class) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcSwitchNetworkComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcSwitchNetworkComponent.kt new file mode 100644 index 0000000000..9bf945bd89 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcSwitchNetworkComponent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.walletconnect.transaction.components.chain + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams +import com.tangem.features.walletconnect.transaction.model.WcSwitchNetworkModel + +internal class WcSwitchNetworkComponent( + appComponentContext: AppComponentContext, + params: WcTransactionModelParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: WcSwitchNetworkModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcHandleMethodErrorConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcHandleMethodErrorConverter.kt index 6c6d317053..93ffa18380 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcHandleMethodErrorConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcHandleMethodErrorConverter.kt @@ -12,5 +12,9 @@ internal object WcHandleMethodErrorConverter : is HandleMethodError.UnknownError, -> WcInnerRoute.UnsupportedMethodAlert HandleMethodError.UnknownSession -> WcInnerRoute.WcDappDisconnected + + is HandleMethodError.NotAddedNetwork -> WcInnerRoute.RequiredAddNetwork(value.networkName) + is HandleMethodError.RequiredNetwork -> WcInnerRoute.RequiredReconnectWithNetwork(value.networkName) + is HandleMethodError.TangemUnsupportedNetwork -> WcInnerRoute.TangemUnsupportedNetwork(value.unsupportedNetwork) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index 348a69439e..c8f65f4c23 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.transaction.converter import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck import com.tangem.domain.walletconnect.usecase.method.WcMethodContext import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep @@ -52,10 +53,19 @@ internal class WcSendTransactionUMConverter @Inject constructor( estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(), isLoading = value.signState.domainStep == WcSignStep.Signing, address = WcAddressConverter.convert(value.context.derivationState), - sendEnabled = value.feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null, + transactionValidationResult = value.securityCheck?.result?.validation, + sendEnabled = when (value.feeState) { + WcTransactionFeeState.None -> feeErrorNotification == null + is WcTransactionFeeState.Success -> { + value.feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null + } + }, feeErrorNotification = feeErrorNotification, ), - feeSelectorUM = value.feeSelectorUM ?: FeeSelectorUM.Loading, + feeSelectorUM = when (value.feeState) { + WcTransactionFeeState.None -> FeeSelectorUM.Loading + is WcTransactionFeeState.Success -> value.feeSelectorUM ?: FeeSelectorUM.Loading + }, transactionRequestInfo = WcTransactionRequestInfoUM( blocks = buildList { addAll( @@ -78,6 +88,7 @@ internal class WcSendTransactionUMConverter @Inject constructor( val actions: WcTransactionActionsUM, val feeSelectorUM: FeeSelectorUM?, val cryptoCurrencyStatus: CryptoCurrencyStatus, + val securityCheck: BlockAidTransactionCheck.Result?, val onFeeReload: () -> Unit, ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt index 86ef260951..e97fc79509 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.entity.send +import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -27,6 +28,7 @@ internal data class WcSendTransactionItemUM( val walletName: String?, val networkInfo: WcNetworkInfoUM, val address: String?, + val transactionValidationResult: ValidationResult?, val sendEnabled: Boolean, val feeErrorNotification: NotificationUM.Info?, val isLoading: Boolean = false, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 1684d836c3..ab284df37a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -49,28 +49,37 @@ internal class WcAddNetworkModel @Inject constructor( private val params = paramsContainer.require() private var useCase by Delegates.notNull() + private val signatureReceivedAnalyticsSendState = MutableStateFlow(false) init { modelScope.launch { useCase = useCaseFactory.createUseCase(params.rawRequest) .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } .getOrNull() ?: return@launch - _uiState.emit( - wcAddEthereumChainUMConverter.convert( - WcAddEthereumChainUMConverter.Input( - useCase = useCase, - actions = WcTransactionActionsUM( - onShowVerifiedAlert = ::showVerifiedAlert, - onDismiss = { cancel(useCase) }, - onSign = { sign(useCase) }, - onCopy = { copyData(useCase.rawSdkRequest.request.params) }, - ), - ), - ), - ) + sendSignatureReceivedAnalytics(useCase) + val either = useCase.invoke() + either + .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } + .map { + if (it.isExistInWcSession) cancel(useCase) else showUI() + } } } + private fun showUI() { + _uiState.value = wcAddEthereumChainUMConverter.convert( + WcAddEthereumChainUMConverter.Input( + useCase = useCase, + actions = WcTransactionActionsUM( + onShowVerifiedAlert = ::showVerifiedAlert, + onDismiss = { cancel(useCase) }, + onSign = { sign(useCase) }, + onCopy = { copyData(useCase.rawSdkRequest.request.params) }, + ), + ), + ) + } + override fun dismiss() { _uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop() } @@ -114,4 +123,18 @@ internal class WcAddNetworkModel @Inject constructor( private fun copyData(text: String) { clipboardManager.setText(text = text, isSensitive = true) } + + private fun sendSignatureReceivedAnalytics(useCase: WcAddNetworkUseCase) { + if (signatureReceivedAnalyticsSendState.value) return + + analytics.send( + WcAnalyticEvents.SignatureRequestReceived( + rawRequest = useCase.rawSdkRequest, + network = useCase.network, + emulationStatus = null, + ), + ) + + signatureReceivedAnalyticsSendState.value = true + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 0e9db77189..d3f10b517e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -18,7 +18,8 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2.Icon.Type -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -26,6 +27,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.SendTransactionError.UserCancelledError import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcAnalyticEvents.SignatureRequestReceived.EmulationStatus @@ -39,6 +41,7 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfigur import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.send.v2.api.subcomponents.feeSelector.entity.FeeSelectorData import com.tangem.features.walletconnect.connections.routing.WcInnerRoute +import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter import com.tangem.features.walletconnect.transaction.converter.WcSendTransactionUMConverter @@ -57,7 +60,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class WcSendTransactionModel @Inject constructor( @@ -112,7 +115,10 @@ internal class WcSendTransactionModel @Inject constructor( ?.dAppFee() ?.let { dAppFee -> feeStateConfiguration = FeeStateConfiguration.Suggestion( - title = stringReference(useCase.session.sdkModel.appMetaData.name), + title = resourceReference( + id = R.string.wc_fee_suggested, + formatArgs = wrappedList(useCase.session.sdkModel.appMetaData.name), + ), fee = dAppFee, ) } @@ -244,6 +250,7 @@ internal class WcSendTransactionModel @Inject constructor( feeSelectorUM = uiState.value?.feeSelectorUM, cryptoCurrencyStatus = cryptoCurrencyStatus, onFeeReload = ::triggerFeeReload, + securityCheck = securityCheck.getOrNull(), ), ) transactionUM = transactionUM?.copy( @@ -275,6 +282,7 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.SelectFee) } + // Before change, make sure you are align with WcTransactionRequestButtons private fun onSign(securityCheck: BlockAidTransactionCheck.Result?) { when (securityCheck?.result?.validation) { ValidationResult.UNSAFE -> showMaliciousAlert(securityCheck.result.description) @@ -339,25 +347,35 @@ internal class WcSendTransactionModel @Inject constructor( } private fun signingIsDone(signState: WcSignState<*>, useCase: WcSignUseCase<*>): Boolean { - (signState.domainStep as? WcSignStep.Result)?.result?.let { - return handleSigningError(it, useCase) + return when (val step = signState.domainStep) { + is WcSignStep.Result -> processResultStep(result = step.result, useCase = useCase) + WcSignStep.PreSign, + WcSignStep.Signing, + -> false } - return false } - private fun handleSigningError(result: Either, useCase: WcSignUseCase<*>): Boolean { - return if (result.isLeft()) { - val error = WcTransactionRoutes.Alert.Type.UnknownError( - errorMessage = result.leftOrNull()?.message(), - onDismiss = { cancel(useCase) }, - onRetry = { signFromAlert() }, - ) - stackNavigation.pushNew(WcTransactionRoutes.Alert(error)) - false - } else { - showSuccessSignMessage() - router.pop() - true + private fun processResultStep(result: Either, useCase: WcSignUseCase<*>): Boolean { + return when (result) { + is Either.Left -> { + val error = result.value + if (error is WcRequestError.WrappedSendError && error.sendTransactionError is UserCancelledError) { + return false + } + + val alertError = WcTransactionRoutes.Alert.Type.UnknownError( + errorMessage = result.value.message(), + onDismiss = { cancel(useCase) }, + onRetry = { signFromAlert() }, + ) + stackNavigation.pushNew(WcTransactionRoutes.Alert(alertError)) + false + } + is Either.Right -> { + showSuccessSignMessage() + router.pop() + true + } } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index 1283f3e84f..9f3a686357 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -59,12 +59,14 @@ internal class WcSignTransactionModel @Inject constructor( val stackNavigation = StackNavigation() private var useCase by Delegates.notNull() + private val signatureReceivedAnalyticsSendState = MutableStateFlow(false) init { modelScope.launch { useCase = useCaseFactory.createUseCase(params.rawRequest) .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } .getOrNull() ?: return@launch + sendSignatureReceivedAnalytics(useCase) useCase.invoke() .onEach { signState -> if (signingIsDone(signState)) return@onEach @@ -129,8 +131,10 @@ internal class WcSignTransactionModel @Inject constructor( } private fun signingIsDone(signState: WcSignState<*>): Boolean { - (signState.domainStep as? WcSignStep.Result)?.result?.let { - showSuccessSignMessage() + (signState.domainStep as? WcSignStep.Result)?.result?.let { result -> + if (result.isRight()) { + showSuccessSignMessage() + } router.pop() return true } @@ -145,4 +149,18 @@ internal class WcSignTransactionModel @Inject constructor( private fun copyData(text: String) { clipboardManager.setText(text = text, isSensitive = true) } + + private fun sendSignatureReceivedAnalytics(useCase: WcMessageSignUseCase) { + if (signatureReceivedAnalyticsSendState.value) return + + analytics.send( + WcAnalyticEvents.SignatureRequestReceived( + rawRequest = useCase.rawSdkRequest, + network = useCase.network, + emulationStatus = null, + ), + ) + + signatureReceivedAnalyticsSendState.value = true + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt new file mode 100644 index 0000000000..d8c49a7508 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt @@ -0,0 +1,51 @@ +package com.tangem.features.walletconnect.transaction.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.walletconnect.WcRequestUseCaseFactory +import com.tangem.domain.walletconnect.model.HandleMethodError +import com.tangem.domain.walletconnect.usecase.method.WcSwitchNetworkUseCase +import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams +import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class WcSwitchNetworkModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val useCaseFactory: WcRequestUseCaseFactory, +) : Model() { + + private val params = paramsContainer.require() + + init { + modelScope.launch { + val useCase = useCaseFactory.createUseCase(params.rawRequest) + .onLeft { showErrorDialog(it) } + .getOrNull() ?: return@launch + val either = useCase.invoke() + useCase.reject() + either + .onLeft { showErrorDialog(it) } + .map { + if (it.isExistInWcSession) { + router.pop() + } else { + showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) + } + } + } + } + + private fun showErrorDialog(error: HandleMethodError) { + router.push(WcHandleMethodErrorConverter.convert(error)) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt index c51cc78fbb..91ab33bb5b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.utils.converter.Converter -import java.math.BigDecimal import javax.inject.Inject internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : @@ -17,7 +16,7 @@ internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : is AmountInfo.FungibleTokens -> WcEstimatedWalletChangeUM( iconRes = value.iconRes, title = resourceReference(value.titleRes), - description = "${value.sign} ${amountInfo.amount.amountText()} ${amountInfo.token.symbol}", + description = "${value.sign} ${amountInfo.amountText()} ${amountInfo.token.symbol}", tokenIconUrl = amountInfo.token.logoUrl, ) is AmountInfo.NonFungibleTokens -> WcEstimatedWalletChangeUM( @@ -29,7 +28,7 @@ internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : } } - private fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } + private fun AmountInfo.FungibleTokens.amountText() = amount.format { crypto("", token.decimals) } data class Input( val amountInfo: AmountInfo, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt index 25e8b53eba..590b49c95e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -132,8 +132,7 @@ private fun EstimatedWalletChangesPreviewMoreThanFour( } } -private class EstimatedWalletChangesPreviewProviderTwoItems : - PreviewParameterProvider { +private class EstimatedWalletChangesPreviewProviderTwoItems : PreviewParameterProvider { override val values = sequenceOf( WcEstimatedWalletChangesUM( items = persistentListOf( @@ -149,6 +148,13 @@ private class EstimatedWalletChangesPreviewProviderTwoItems : description = "+ 1,131.46 MATIC", tokenIconUrl = "https://tangem.com", ), + WcEstimatedWalletChangeUM( + iconRes = R.drawable.img_approvale_new_24, + title = resourceReference(R.string.common_approve), + description = "10 Collection", + tokenIconUrl = "https://cdn.blockaid.io/nft/0x09851531816f78cF4841f1DeF22fbaB78aDD02c5/29805/" + + "polygon?r=ed117da0-6065-4ff8-ba81-cb7395e1ec3d", + ), ), ), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 1eda478462..3ef6afbf4c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -4,6 +4,7 @@ import com.domain.blockaid.models.transaction.CheckTransactionResult import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto @@ -21,7 +22,7 @@ import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import javax.inject.Inject -internal const val DECIMALS_AMOUNT = 2 +private const val DECIMALS_AMOUNT = 2 @Suppress("CyclomaticComplexMethod", "LongMethod") internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( @@ -55,7 +56,20 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( }, estimatedWalletChanges = (simulation as? SimulationResult.Success)?.data?.let { data -> when (data) { - is SimulationData.Approve, SimulationData.NoWalletChangesDetected -> null + is SimulationData.NoWalletChangesDetected -> null + is SimulationData.Approve -> { + val nftItems = data.items.mapNotNull { item -> + if (item !is ApproveInfo.NonFungibleToken) return@mapNotNull null + + WcEstimatedWalletChangeUM( + iconRes = R.drawable.img_approvale_new_24, + title = TextReference.Res(R.string.common_approve), + description = item.name, + tokenIconUrl = item.logoUrl, + ) + } + WcEstimatedWalletChangesUM(nftItems.toImmutableList()).takeIf { nftItems.isNotEmpty() } + } is SimulationData.SendAndReceive -> { val items: ImmutableList = ( data.send.map { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt index e835f5ad87..73d7ef80e2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt @@ -30,8 +30,8 @@ import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.chain.WcAddEthereumChainItemUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM +import com.tangem.features.walletconnect.transaction.ui.common.WcSimpleConfirmButtons import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import com.tangem.features.walletconnect.transaction.ui.common.WcWalletItem @@ -52,7 +52,7 @@ internal fun WcAddEthereumChainModalBottomSheetContent( onBack = onBack, title = { TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = resourceReference(R.string.wc_transaction_flow_title), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, ) @@ -91,11 +91,11 @@ internal fun WcAddEthereumChainModalBottomSheetContent( } }, footer = { - WcTransactionRequestButtons( + WcSimpleConfirmButtons( modifier = Modifier.padding(16.dp), onDismiss = state.onDismiss, onClickActiveButton = state.onSign, - activeButtonText = resourceReference(R.string.common_sign), + activeButtonText = resourceReference(R.string.common_add), isLoading = state.isLoading, ) }, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt index 35e9a28c76..b8c0eb3057 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt @@ -30,7 +30,7 @@ internal fun WcAddressItem(address: String, modifier: Modifier = Modifier) { ) Text( modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - text = stringResourceSafe(R.string.wc_common_address), + text = stringResourceSafe(R.string.common_address), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, maxLines = 1, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index ed0217aee9..61f1cf95e7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -2,9 +2,12 @@ package com.tangem.features.walletconnect.transaction.ui.common import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.domain.blockaid.models.transaction.ValidationResult +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.extensions.TextReference @@ -17,10 +20,78 @@ import com.tangem.features.walletconnect.impl.R internal fun WcTransactionRequestButtons( activeButtonText: TextReference, isLoading: Boolean, + validationResult: ValidationResult?, onDismiss: () -> Unit, onClickActiveButton: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, +) { + WcCommonButtons( + onDismiss = onDismiss, + modifier = modifier, + primaryButton = { + // Before change, make sure you are align with WcSendTransactionModel::onSign + when (validationResult) { + ValidationResult.UNSAFE, + ValidationResult.WARNING, + -> PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.common_continue), + onClick = onClickActiveButton, + showProgress = isLoading, + enabled = enabled, + ) + ValidationResult.SAFE, + ValidationResult.FAILED_TO_VALIDATE, + null, + -> PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = activeButtonText.resolveReference(), + onClick = onClickActiveButton, + iconResId = R.drawable.ic_tangem_24, + showProgress = isLoading, + enabled = enabled, + ) + } + }, + ) +} + +@Composable +internal fun WcSimpleConfirmButtons( + activeButtonText: TextReference, + isLoading: Boolean, + onDismiss: () -> Unit, + onClickActiveButton: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + WcCommonButtons( + onDismiss = onDismiss, + modifier = modifier, + primaryButton = { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = activeButtonText.resolveReference(), + onClick = onClickActiveButton, + showProgress = isLoading, + enabled = enabled, + ) + }, + ) +} + +@Composable +internal fun WcCommonButtons( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + primaryButton: @Composable RowScope.() -> Unit, ) { Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { SecondaryButton( @@ -30,15 +101,6 @@ internal fun WcTransactionRequestButtons( text = stringResourceSafe(R.string.common_cancel), onClick = onDismiss, ) - PrimaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - text = activeButtonText.resolveReference(), - onClick = onClickActiveButton, - iconResId = R.drawable.ic_tangem_24, - showProgress = isLoading, - enabled = enabled, - ) + primaryButton() } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index cf5f3fee9f..b647ef5abd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp +import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -24,6 +25,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -71,7 +73,7 @@ internal fun WcSendTransactionModalBottomSheet( containerColor = TangemTheme.colors.background.tertiary, title = { config -> TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = resourceReference(R.string.wc_transaction_flow_title), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, ) @@ -122,7 +124,7 @@ internal fun WcSendTransactionModalBottomSheet( modifier = Modifier.padding(top = 14.dp), config = state.feeErrorNotification.config, iconTint = TangemTheme.colors.icon.warning, - containerColor = TangemTheme.colors.button.disabled, + containerColor = TangemTheme.colors.background.action, ) } } @@ -136,6 +138,7 @@ internal fun WcSendTransactionModalBottomSheet( activeButtonText = resourceReference(R.string.common_send), isLoading = state.isLoading, enabled = state.sendEnabled, + validationResult = state.transactionValidationResult, ) }, ) @@ -220,6 +223,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide address = null, sendEnabled = true, feeErrorNotification = null, + transactionValidationResult = null, ), WcSendTransactionItemUM( onDismiss = {}, @@ -262,6 +266,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide title = stringReference("Insufficient Ethereum"), subtitle = stringReference("Top up your balance to cover the network fee"), ), + transactionValidationResult = ValidationResult.WARNING, ), WcSendTransactionItemUM( onDismiss = {}, @@ -301,9 +306,14 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide address = null, sendEnabled = false, feeErrorNotification = NotificationUM.Info( - title = stringReference("Insufficient Ethereum"), - subtitle = stringReference("Top up your balance to cover the network fee"), + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = {}, + ), ), + transactionValidationResult = ValidationResult.SAFE, ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 86e7fad3f4..5b0b50fe2e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -50,7 +50,7 @@ internal fun WcSignTransactionModalBottomSheetContent( onBack = onBack, title = { TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = resourceReference(R.string.wc_transaction_flow_title), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, ) @@ -95,6 +95,7 @@ internal fun WcSignTransactionModalBottomSheetContent( onClickActiveButton = state.onSign, activeButtonText = resourceReference(R.string.common_sign), isLoading = state.isLoading, + validationResult = null, ) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt index 8867a92b5d..09b3659efd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt @@ -1,6 +1,5 @@ package com.tangem.features.walletconnect.utils -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference @@ -54,14 +53,11 @@ internal class WcNotificationsFactory @Inject constructor() { feeSelectorUM: FeeSelectorUM?, ): Boolean { val feeSelectorContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false - val lowestFee = when (val fees = feeSelectorContent.fees) { - is TransactionFee.Choosable -> fees.minimum - is TransactionFee.Single -> fees.normal - } + val selectedFee = feeSelectorContent.selectedFeeItem.fee return FeeCalculationUtils.checkExceedBalance( feeBalance = cryptoCurrencyStatus.value.amount, - feeAmount = lowestFee.amount.value, + feeAmount = selectedFee.amount.value, ) } } \ No newline at end of file diff --git a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt index 6d9044f216..bc7092a29c 100644 --- a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt +++ b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt @@ -1,16 +1,9 @@ package com.tangem.features.welcome -import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { - data class Params( - val launchMode: InitScreenLaunchMode, - val intent: SerializableIntent?, - ) - - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 499c0ab82e..7557118256 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -13,11 +13,14 @@ android { dependencies { implementation(projects.features.welcome.api) + implementation(projects.features.wallet.api) /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.analytics) implementation(projects.common.routing) implementation(projects.common.ui) @@ -30,6 +33,8 @@ dependencies { /** Domain */ implementation(projects.domain.appCurrency) implementation(projects.domain.wallets) + implementation(projects.domain.card) + implementation(projects.domain.settings) /** DI */ implementation(deps.hilt.android) @@ -54,4 +59,5 @@ dependencies { implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt index 7bc6a1d982..8638078499 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt @@ -15,10 +15,10 @@ import dagger.assisted.AssistedInject internal class DefaultWelcomeComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: WelcomeComponent.Params, + @Assisted val params: Unit, ) : WelcomeComponent, AppComponentContext by context { - private val model: WelcomeModel = getOrCreateModel(params) + private val model: WelcomeModel = getOrCreateModel() @Composable override fun Content(modifier: Modifier) { @@ -32,6 +32,6 @@ internal class DefaultWelcomeComponent @AssistedInject constructor( @AssistedFactory interface Factory : WelcomeComponent.Factory { - override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent + override fun create(context: AppComponentContext, params: Unit): DefaultWelcomeComponent } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index da1f8c9c21..3baa86b20e 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -1,18 +1,277 @@ package com.tangem.features.welcome.impl.model +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val uiMessageSender: UiMessageSender, + private val userWalletsListRepository: UserWalletsListRepository, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val walletsRepository: WalletsRepository, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + userWalletsFetcherFactory: UserWalletsFetcher.Factory, ) : Model() { val uiState: StateFlow - field = MutableStateFlow(WelcomeUM.Plain) + field = MutableStateFlow(WelcomeUM.Plain) + + private val walletsFetcher = userWalletsFetcherFactory.create( + messageSender = uiMessageSender, + onlyMultiCurrency = false, + authMode = true, + onWalletClick = { walletId -> + modelScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first { it.walletId == walletId } + onUserWalletClick(userWallet) + } + }, + ) + private val walletsFetcherJobHolder = JobHolder() + private val wallets = MutableStateFlow>(persistentListOf()) + private var routedOut = false + + init { + modelScope.launch { + userWalletsListRepository.load() + wallets.value = walletsFetcher.userWallets.first() + + launch { + walletsFetcher.userWallets + .collectLatest { + if (it.isEmpty()) { + router.replaceAll(AppRoute.Home()) + } + + wallets.value = it + } + } + + tryToUnlockRightAway() + } + } + + private fun tryToUnlockRightAway() { + modelScope.launch { + if (canUnlockWithBiometrics()) { + userWalletsListRepository.unlockAllWallets() + .onRight { + routedOut = true + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }) + setSelectWalletState() + } + } else { + tryToUnlockWithAccessCodeRightAway() + setSelectWalletState() + } + } + } + + private suspend fun tryToUnlockWithAccessCodeRightAway() { + if (onlyOneHotWalletWithAccessCode()) { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first() + uiState.value = WelcomeUM.Empty + unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode) + } + } + + private fun setSelectWalletState() { + modelScope.launch { + if (routedOut || uiState.value is WelcomeUM.SelectWallet) return@launch + + uiState.value = WelcomeUM.SelectWallet( + wallets = walletsFetcher.userWallets.first(), + showUnlockWithBiometricButton = canUnlockWithBiometrics(), + addWalletClick = ::addWalletClick, + onUnlockWithBiometricClick = { + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onRight { + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { /* ignore */ }) + } + } + }, + ) + + wallets.collectLatest { wallets -> + updateSelectState { + it.copy(wallets = wallets) + } + } + }.saveIn(walletsFetcherJobHolder) + } + + private fun addWalletClick() { + updateSelectState { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(R.string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(R.string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(R.string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + onAddWalletOptionClick(optionKey) + }, + ), + onDismissRequest = { + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + }, + ), + ) + } + } + + private fun onAddWalletOptionClick(optionKey: String) { + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + } + + private suspend fun onlyOneHotWalletWithAccessCode(): Boolean { + val userWalletsWithLock = userWalletsListRepository.userWalletsSync().filter { it.isLocked } + if (userWalletsWithLock.size != 1) return false + val wallet = userWalletsWithLock.first() + return wallet is UserWallet.Hot && wallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + } + + private fun onUserWalletClick(userWallet: UserWallet) = modelScope.launch { + if (userWallet.isLocked.not()) { + // If the wallet is not locked, we can proceed to the wallet screen directly + userWalletsListRepository.select(userWallet.walletId) + router.replaceAll(AppRoute.Wallet) + return@launch + } + + val unlockMethod = when (userWallet) { + is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan + is UserWallet.Hot -> { + uiState.value = WelcomeUM.Empty + UserWalletsListRepository.UnlockMethod.AccessCode + } + } + + unlockWallet(userWallet.walletId, unlockMethod) + setSelectWalletState() + } + + private suspend fun canUnlockWithBiometrics(): Boolean { + return canUseBiometryUseCase() && walletsRepository.useBiometricAuthentication() + } + + suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { + userWalletsListRepository.unlock(userWalletId, unlockMethod) + .onRight { + routedOut = true + userWalletsListRepository.select(userWalletId) + router.replaceAll(AppRoute.Wallet) + } + .onLeft { error -> + error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + } + } + + suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { + when (this) { + UnlockWalletError.AlreadyUnlocked -> { + // this should not happen, as we check for locked state before this + specificWalletId?.let { userWalletsListRepository.select(it) } + router.replaceAll(AppRoute.Wallet) + } + UnlockWalletError.ScannedCardWalletNotMatched -> { + // TODO Scanned card does not match the wallet + } + UnlockWalletError.UnableToUnlock -> { + // TODO Unable to unlock the wallet" + } + UnlockWalletError.UserCancelled -> onUserCancelled() + UnlockWalletError.UserWalletNotFound -> { + // This should never happen in this flow, as we always check for the wallet existence before unlocking + Timber.e("User wallet not found for unlock: $specificWalletId") + uiMessageSender.send( + SnackbarMessage(TextReference.Res(R.string.generic_error)), + ) + } + } + } + + private fun updateSelectState(block: (WelcomeUM.SelectWallet) -> WelcomeUM.SelectWallet) { + uiState.update { currentState -> + if (currentState is WelcomeUM.SelectWallet) { + block(currentState) + } else { + currentState + } + } + } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt deleted file mode 100644 index 4331461929..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.inputrow.InputRowDefault -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM - -@Composable -fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - titleText = TextReference.Str("Add Wallet"), - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: AddWalletBottomSheetContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - InputRowDefault( - text = TextReference.Str("Create New Wallet"), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 0, - lastIndex = 3, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, - ) - InputRowDefault( - text = TextReference.Str("Add Existing Wallet"), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 1, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, - ) - InputRowDefault( - text = TextReference.Str("Buy Tangem Wallet"), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 2, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) }, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - AddWalletBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = AddWalletBottomSheetContentUM(), - ), - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt index caf6ecd9bd..04b1e81910 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -10,10 +10,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.extensions.TextReference +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.ui.state.WalletUM +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.persistentListOf @@ -34,10 +35,7 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { state = st, modifier = modifier, ) - is WelcomeUM.EnterAccessCode -> WelcomeEnterAccessCode( - state = st, - modifier = modifier, - ) + WelcomeUM.Empty -> {} } } } @@ -49,22 +47,30 @@ private fun Preview() { TangemThemePreview { val state = WelcomeUM.SelectWallet( wallets = persistentListOf( - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("3 cards"), - imageState = WalletUM.ImageState.Loading, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Loading, + balance = UserWalletItemUM.Balance.Loaded( + value = "1.2345 BTC", + isFlickering = false, + ), + isEnabled = true, onClick = {}, ), - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("Mobile wallet"), - imageState = WalletUM.ImageState.MobileWallet, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Failed, + imageState = UserWalletItemUM.ImageState.MobileWallet, + balance = UserWalletItemUM.Balance.Locked, + isEnabled = true, onClick = {}, ), ), ) - var currentState by remember { mutableStateOf(WelcomeUM.EnterAccessCode()) } + var currentState by remember { mutableStateOf(WelcomeUM.SelectWallet()) } Box { Welcome(currentState) @@ -74,11 +80,8 @@ private fun Preview() { onClick = { currentState = when (currentState) { is WelcomeUM.Plain -> state - is WelcomeUM.SelectWallet -> WelcomeUM.EnterAccessCode( - value = "", - onValueChange = {}, - ) - is WelcomeUM.EnterAccessCode -> WelcomeUM.Plain + is WelcomeUM.SelectWallet -> WelcomeUM.Empty + WelcomeUM.Empty -> WelcomeUM.Plain } }, ) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt deleted file mode 100644 index 3cd6085b58..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.appbar.TopAppBarButton -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.welcome.impl.ui.state.WelcomeUM - -@Suppress("MagicNumber") -@Composable -internal fun AnimatedContentScope.WelcomeEnterAccessCode( - state: WelcomeUM.EnterAccessCode, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxSize() - .statusBarsPadding(), - ) { - Column { - TopAppBarButton( - modifier = Modifier - .padding(12.dp), - button = TopAppBarButtonUM.Back(onBackClicked = state.onBackClick), - tint = TangemTheme.colors.icon.primary1, - ) - - SpacerH(68.dp) - - Text( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .align(Alignment.CenterHorizontally), - text = "Enter Access Code", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - - SpacerH24() - - Box( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - PinTextField( - length = 6, - isPasswordVisual = true, - value = state.value, - onValueChange = state.onValueChange, - ) - } - } - - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .imePadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Log in with biometric", - onClick = state.onUnlockWithBiometricClick, - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt index 13aaae14db..5b80e192ee 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt @@ -9,8 +9,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.welcome.impl.R @Composable @@ -26,4 +28,14 @@ internal fun WelcomePlain(modifier: Modifier = Modifier) { contentDescription = null, ) } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + WelcomePlain( + modifier = Modifier.fillMaxSize(), + ) + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index ed41fe5ad4..7e9e671696 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -5,12 +5,9 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -20,14 +17,18 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp -import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.WalletUM import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -45,7 +46,7 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal TitleText() SpacerH12() - var actualWallets by remember { mutableStateOf>(persistentListOf()) } + var actualWallets by remember { mutableStateOf>(persistentListOf()) } Box(modifier = Modifier.weight(1f)) { LazyColumn( @@ -62,25 +63,33 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal verticalArrangement = Arrangement.spacedBy(8.dp), ) { itemsIndexed(actualWallets) { index, walletState -> - WalletItem( + UserWalletItem( + modifier = Modifier.fillMaxWidth(), state = walletState, - modifier = Modifier, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.field.primary, + ), ) } } BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Unlock all with biometric", - onClick = state.onUnlockWithBiometricClick, - ) + if (state.showUnlockWithBiometricButton) { + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .navigationBarsPadding() + .animateEnterExit(fadeIn(), fadeOut()), + text = stringResourceSafe( + R.string.user_wallet_list_unlock_all_with, + stringResourceSafe(id = R.string.common_biometrics), + ), + onClick = state.onUnlockWithBiometricClick, + ) + } } LaunchedEffect(state.wallets) { @@ -121,7 +130,7 @@ private fun AnimatedContentScope.TopBar(state: WelcomeUM.SelectWallet, modifier: TextButton( modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge), - text = "Add Wallet", + text = stringResourceSafe(R.string.auth_info_add_wallet_title), colors = TangemButtonsDefaults.defaultTextButtonColors.copy( contentColor = TangemTheme.colors.text.primary1, ), @@ -145,7 +154,7 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Welcome back!", + text = stringResourceSafe(R.string.auth_info_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -160,71 +169,18 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Select a wallet to log in", + text = stringResourceSafe(R.string.auth_info_subtitle), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, ) } } -@Suppress("MagicNumber") @Composable -private fun WalletItem(state: WalletUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.secondary, TangemTheme.shapes.roundedCornersXMedium) - .clickable(onClick = state.onClick) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - WalletImage(state.imageState) - - SpacerW12() - - Column(Modifier.weight(1f)) { - Text( - text = state.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - - Text( - text = state.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun WalletImage(state: WalletUM.ImageState, modifier: Modifier = Modifier) { - when (state) { - WalletUM.ImageState.MobileWallet -> { - Box( - modifier = modifier - .size(36.dp) - .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_wallet_filled_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - } - else -> { - CardImage( - imageState = when (state) { - is WalletUM.ImageState.Image -> UserWalletItemUM.ImageState.Image(state.artwork) - WalletUM.ImageState.Loading -> UserWalletItemUM.ImageState.Loading - else -> error("") - }, - modifier = modifier, - ) - } - } +fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt deleted file mode 100644 index 216a28f7d2..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.welcome.impl.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.TextReference -import javax.annotation.concurrent.Immutable - -internal data class WalletUM( - val name: TextReference, - val subtitle: TextReference, - val imageState: ImageState, - val onClick: () -> Unit, -) { - - @Immutable - sealed class ImageState { - data object MobileWallet : ImageState() - data object Loading : ImageState() - data class Image( - val artwork: ArtworkUM, - ) : ImageState() - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt index 1e2e509d58..c86bd0befa 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.welcome.impl.ui.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -8,20 +9,15 @@ import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class WelcomeUM { + data object Empty : WelcomeUM() + data object Plain : WelcomeUM() data class SelectWallet( - val wallets: ImmutableList = persistentListOf(), + val wallets: ImmutableList = persistentListOf(), val showUnlockWithBiometricButton: Boolean = false, val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, val onUnlockWithBiometricClick: () -> Unit = {}, val addWalletClick: () -> Unit = {}, ) : WelcomeUM() - - data class EnterAccessCode( - val value: String = "", - val onUnlockWithBiometricClick: () -> Unit = {}, - val onValueChange: (String) -> Unit = {}, - val onBackClick: () -> Unit = {}, - ) : WelcomeUM() } \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b1088cbc11..418aa40d43 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1140" +tangemBlockchainSdk = "releases-5.28-1206" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-511" +tangemCardSdk = "releases-5.28-559" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-461" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index f15198c725..12aeb18278 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -15,10 +15,8 @@ import com.tangem.crypto.hdWallet.DerivationPath 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.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt index 17f61ecdd2..058a4ccb06 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -15,13 +15,6 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v value = "\"$value\"", ) - // TODO remove - class TestActionEnabled(isEnabled: Boolean) : BuildConfigField( - type = "Boolean", - name = "TEST_ACTION_ENABLED", - value = isEnabled.toString(), - ) - class LogEnabled(isEnabled: Boolean) : BuildConfigField( type = "Boolean", name = "LOG_ENABLED", diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 91ee0ee9d8..896952a0e8 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -25,7 +25,6 @@ internal enum class BuildType( appIdSuffix = "debug", configFields = listOf( BuildConfigField.Environment(value = "dev"), - BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = false), @@ -47,7 +46,6 @@ internal enum class BuildType( versionSuffix = "mocked", configFields = listOf( BuildConfigField.Environment(value = "dev"), - BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = true), @@ -71,7 +69,6 @@ internal enum class BuildType( versionSuffix = "internal", configFields = listOf( BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = false), @@ -92,7 +89,6 @@ internal enum class BuildType( versionSuffix = "external", configFields = listOf( BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), BuildConfigField.MockDataSource(isEnabled = false), @@ -111,7 +107,6 @@ internal enum class BuildType( id = "release", configFields = listOf( BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), BuildConfigField.MockDataSource(isEnabled = false), diff --git a/settings.gradle.kts b/settings.gradle.kts index 9e2dd16bde..d785e00c38 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -260,10 +260,16 @@ include(":features:walletconnect:impl") include(":features:hot-wallet:api") include(":features:hot-wallet:impl") +include(":features:kyc:api") //TODO disable for release because of the permissions -// include(":features:kyc:api") // include(":features:kyc:impl") +include(":features:tangempay:main:api") +include(":features:tangempay:main:impl") + +include(":features:tangempay:details:api") +include(":features:tangempay:details:impl") + include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl")