Updated on 2026-08-14
This commit is contained in:
commit
870c3fa9d3
291 changed files with 4985 additions and 5870 deletions
|
|
@ -167,6 +167,7 @@ dependencies {
|
||||||
implementation(projects.domain.blockaid)
|
implementation(projects.domain.blockaid)
|
||||||
implementation(projects.domain.hotWallet)
|
implementation(projects.domain.hotWallet)
|
||||||
implementation(projects.domain.news)
|
implementation(projects.domain.news)
|
||||||
|
implementation(projects.domain.earn)
|
||||||
|
|
||||||
implementation(projects.common)
|
implementation(projects.common)
|
||||||
implementation(projects.common.routing)
|
implementation(projects.common.routing)
|
||||||
|
|
@ -220,6 +221,7 @@ dependencies {
|
||||||
implementation(projects.data.yieldSupply)
|
implementation(projects.data.yieldSupply)
|
||||||
implementation(projects.data.hotWallet)
|
implementation(projects.data.hotWallet)
|
||||||
implementation(projects.data.news)
|
implementation(projects.data.news)
|
||||||
|
implementation(projects.data.earn)
|
||||||
|
|
||||||
/** Features */
|
/** Features */
|
||||||
implementation(projects.features.referral.impl)
|
implementation(projects.features.referral.impl)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ abstract class BaseTestCase : TestCase(
|
||||||
* – and only after that the activity should be launched.
|
* – and only after that the activity should be launched.
|
||||||
*/
|
*/
|
||||||
protected fun setupHooks(
|
protected fun setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection: () -> Unit = {},
|
||||||
additionalBeforeSection: () -> Unit = {},
|
additionalBeforeSection: () -> Unit = {},
|
||||||
additionalAfterSection: () -> Unit = {},
|
additionalAfterSection: () -> Unit = {},
|
||||||
) = before {
|
) = before {
|
||||||
|
|
@ -91,6 +92,7 @@ abstract class BaseTestCase : TestCase(
|
||||||
// Setup WireMock redirect for CI with local WireMock instances
|
// Setup WireMock redirect for CI with local WireMock instances
|
||||||
val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG)
|
val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG)
|
||||||
WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl
|
WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl
|
||||||
|
additionalBeforeAppLaunchSection()
|
||||||
hiltRule.inject()
|
hiltRule.inject()
|
||||||
runBlocking {
|
runBlocking {
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
appPreferencesStore.editData { mutablePreferences ->
|
||||||
|
|
@ -147,7 +149,9 @@ abstract class BaseTestCase : TestCase(
|
||||||
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
||||||
"HOT_WALLET_ENABLED" to true,
|
"HOT_WALLET_ENABLED" to true,
|
||||||
"YIELD_SUPPLY_FEATURE_ENABLED" to true,
|
"YIELD_SUPPLY_FEATURE_ENABLED" to true,
|
||||||
"ACCOUNTS_FEATURE_ENABLED" to true
|
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||||
|
"FEED_ENABLED" to true,
|
||||||
|
"GASLESS_TRANSACTIONS_ENABLED" to true,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package com.tangem.common.extensions
|
package com.tangem.common.extensions
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsMatcher
|
||||||
|
import com.tangem.common.utils.LazyListItemNode
|
||||||
|
import com.tangem.core.ui.components.buttons.actions.HasBadgeKey
|
||||||
|
import com.tangem.core.ui.components.buttons.actions.IsDimmedKey
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
|
||||||
fun assertElementDoesNotExist(
|
fun assertElementDoesNotExist(
|
||||||
|
|
@ -22,4 +26,20 @@ fun assertElementDoesNotExist(
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Any.assertIsDimmed(expectedValue: Boolean = true) {
|
||||||
|
val matcher = SemanticsMatcher.expectValue(IsDimmedKey, expectedValue)
|
||||||
|
when (this) {
|
||||||
|
is KNode, is LazyListItemNode -> this.assert(matcher)
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type: ${this::class}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Any.assertHasBadge(expectedValue: Boolean = true) {
|
||||||
|
val matcher = SemanticsMatcher.expectValue(HasBadgeKey, expectedValue)
|
||||||
|
when (this) {
|
||||||
|
is KNode, is LazyListItemNode -> this.assert(matcher)
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type: ${this::class}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.common.extensions
|
||||||
|
|
||||||
import androidx.test.uiautomator.By
|
import androidx.test.uiautomator.By
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
|
||||||
|
|
@ -74,8 +75,17 @@ fun BaseTestCase.stopApp(packageName: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BaseTestCase.launchApp(packageName: String) {
|
fun BaseTestCase.launchApp(packageName: String) {
|
||||||
device.apps.waitForAppLaunchAndReady(packageName = packageName)
|
|
||||||
device.apps.launch(packageName)
|
device.apps.launch(packageName)
|
||||||
|
device.apps.waitForAppLaunchAndReady(packageName = packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.restartApp(packageName: String) {
|
||||||
|
device.uiDevice.pressHome()
|
||||||
|
device.apps.kill(packageName)
|
||||||
|
waitForIdle()
|
||||||
|
device.apps.launch(packageName)
|
||||||
|
device.apps.waitForAppLaunchAndReady(timeout = WAIT_UNTIL_TIMEOUT_LONG, packageName = packageName)
|
||||||
|
waitForIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SwipeDirection {
|
enum class SwipeDirection {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.tangem.scenarios
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.screens.onMarketsScreen
|
||||||
|
import com.tangem.screens.onMarketsTokenDetailsScreen
|
||||||
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
|
||||||
|
fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName: String) {
|
||||||
|
step("Open 'Markets' screen") {
|
||||||
|
onMainScreen { searchThroughMarketPlaceholder.performClick() }
|
||||||
|
waitForIdle()
|
||||||
|
}
|
||||||
|
step("Click on 'Search' placeholder") {
|
||||||
|
onMarketsScreen { searchThroughMarketPlaceholder.performClick() }
|
||||||
|
}
|
||||||
|
step("Click on $blockchainName blockchain") {
|
||||||
|
waitForIdle()
|
||||||
|
onMarketsScreen { tokenWithTitle(blockchainName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Click on $tokenName token") {
|
||||||
|
waitForIdle()
|
||||||
|
onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
}
|
||||||
180
app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt
Normal file
180
app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
package com.tangem.scenarios
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.click
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.Allure.step
|
||||||
|
import com.tangem.common.ui.R as CommonUiR
|
||||||
|
|
||||||
|
private val firstStoryIndex = 0
|
||||||
|
private val firstStoryTitle = getResourceString(CommonUiR.string.swap_story_first_title)
|
||||||
|
private val firstStorySubtitle = getResourceString(CommonUiR.string.swap_story_first_subtitle)
|
||||||
|
private val secondStoryIndex = 1
|
||||||
|
private val secondStoryTitle = getResourceString(CommonUiR.string.swap_story_second_title)
|
||||||
|
private val secondStorySubtitle = getResourceString(CommonUiR.string.swap_story_second_subtitle)
|
||||||
|
private val thirdStoryIndex = 2
|
||||||
|
private val thirdStoryTitle = getResourceString(CommonUiR.string.swap_story_third_title)
|
||||||
|
private val thirdStorySubtitle = getResourceString(CommonUiR.string.swap_story_third_subtitle)
|
||||||
|
private val forthStoryIndex = 3
|
||||||
|
private val forthStoryTitle = getResourceString(CommonUiR.string.swap_story_forth_title)
|
||||||
|
private val forthStorySubtitle = getResourceString(CommonUiR.string.swap_story_forth_subtitle)
|
||||||
|
private val fifthStoryIndex = 4
|
||||||
|
private val fifthStoryTitle = getResourceString(CommonUiR.string.swap_story_fifth_title)
|
||||||
|
private val fifthStorySubtitle = getResourceString(CommonUiR.string.swap_story_fifth_subtitle)
|
||||||
|
|
||||||
|
fun BaseTestCase.openSwapScreen(
|
||||||
|
from: SwapEntryPoint,
|
||||||
|
storiesExist: Boolean = true,
|
||||||
|
) {
|
||||||
|
when (from) {
|
||||||
|
SwapEntryPoint.MainScreen -> step("Click on 'Swap' button on 'Main' screen") {
|
||||||
|
onMainScreen { swapButton.performClick() }
|
||||||
|
}
|
||||||
|
|
||||||
|
SwapEntryPoint.TokenDetails -> step("Click on 'Swap' button on 'Token details' screen") {
|
||||||
|
onTokenDetailsScreen { swapButton().performClick() }
|
||||||
|
}
|
||||||
|
|
||||||
|
SwapEntryPoint.MarketsTokenDetails -> step("Click on 'Swap' button on 'Markets' token details screen") {
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() }
|
||||||
|
}
|
||||||
|
|
||||||
|
SwapEntryPoint.TokenActionsBottomSheet -> step("Click on 'Swap' button on token actions bottom sheet") {
|
||||||
|
onTokenActionsBottomSheet { swapButton.performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storiesExist) {
|
||||||
|
step("Close 'Stories' screen") {
|
||||||
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
step("Assert 'Stories' screen is not displayed") {
|
||||||
|
onSwapStoriesScreen { container.assertDoesNotExist() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step("Assert 'Swap' screen title is displayed") {
|
||||||
|
onSwapTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.checkStoriesContent(
|
||||||
|
storyIndex: Int,
|
||||||
|
storyTitle: String,
|
||||||
|
storySubtitle: String,
|
||||||
|
) {
|
||||||
|
step("Assert 'Close' button is displayed") {
|
||||||
|
onSwapStoriesScreen { closeButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert progress bar item №${storyIndex + 1} is displayed") {
|
||||||
|
onSwapStoriesScreen { progressBarItem(storyIndex).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert story title is $storyTitle") {
|
||||||
|
onSwapStoriesScreen { title.assertTextContains(storyTitle) }
|
||||||
|
}
|
||||||
|
step("Assert story subtitle is $storySubtitle") {
|
||||||
|
onSwapStoriesScreen { subtitle.assertTextContains(storySubtitle) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.checkStoriesChanges() {
|
||||||
|
step("Check title and subtitle for story №${firstStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = firstStoryIndex,
|
||||||
|
storyTitle = firstStoryTitle,
|
||||||
|
storySubtitle = firstStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on right side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerRight) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${secondStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = secondStoryIndex,
|
||||||
|
storyTitle = secondStoryTitle,
|
||||||
|
storySubtitle = secondStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on right side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerRight) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${thirdStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = thirdStoryIndex,
|
||||||
|
storyTitle = thirdStoryTitle,
|
||||||
|
storySubtitle = thirdStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on right side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerRight) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${forthStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = forthStoryIndex,
|
||||||
|
storyTitle = forthStoryTitle,
|
||||||
|
storySubtitle = forthStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on right side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerRight) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${fifthStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = fifthStoryIndex,
|
||||||
|
storyTitle = fifthStoryTitle,
|
||||||
|
storySubtitle = fifthStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on left side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${forthStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = forthStoryIndex,
|
||||||
|
storyTitle = forthStoryTitle,
|
||||||
|
storySubtitle = forthStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on left side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${thirdStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = thirdStoryIndex,
|
||||||
|
storyTitle = thirdStoryTitle,
|
||||||
|
storySubtitle = thirdStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on left side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${secondStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = secondStoryIndex,
|
||||||
|
storyTitle = secondStoryTitle,
|
||||||
|
storySubtitle = secondStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
step("Click on left side") {
|
||||||
|
onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } }
|
||||||
|
}
|
||||||
|
step("Check title and subtitle for story №${firstStoryIndex + 1}") {
|
||||||
|
checkStoriesContent(
|
||||||
|
storyIndex = firstStoryIndex,
|
||||||
|
storyTitle = firstStoryTitle,
|
||||||
|
storySubtitle = firstStorySubtitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SwapEntryPoint {
|
||||||
|
object MainScreen : SwapEntryPoint()
|
||||||
|
object TokenDetails : SwapEntryPoint()
|
||||||
|
object MarketsTokenDetails : SwapEntryPoint()
|
||||||
|
object TokenActionsBottomSheet : SwapEntryPoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -45,6 +45,16 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
hasText(getResourceString(R.string.common_continue))
|
hasText(getResourceString(R.string.common_continue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val addButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasText(getResourceString(R.string.common_add))
|
||||||
|
}
|
||||||
|
|
||||||
|
val laterButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
|
hasText(getResourceString(R.string.common_later))
|
||||||
|
}
|
||||||
|
|
||||||
val okButton: KNode = child {
|
val okButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||||
hasText(getResourceString(R.string.common_ok))
|
hasText(getResourceString(R.string.common_ok))
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.markets_search_header_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
fun tokenNetworkGroupTitle(tokenNetwork: String): KNode {
|
||||||
return lazyList.child {
|
return lazyList.child {
|
||||||
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM)
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,44 @@
|
||||||
package com.tangem.screens
|
package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.test.SemanticsMatcher
|
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
|
import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX
|
||||||
import com.tangem.common.utils.LazyListItemNode
|
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.test.MarketsTestTags
|
import com.tangem.core.ui.test.MarketsTestTags
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
|
||||||
import com.tangem.features.onramp.impl.R
|
import com.tangem.features.onramp.impl.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
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.ComposeScreen.Companion.onComposeScreen
|
||||||
import io.github.kakaocup.compose.node.element.KNode
|
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.kakao.common.utilities.getResourceString
|
||||||
import androidx.compose.ui.test.hasText as withText
|
|
||||||
|
|
||||||
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<MarketsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
private val lazyList = KLazyListNode(
|
|
||||||
semanticsProvider = semanticsProvider,
|
|
||||||
viewBuilderAction = { hasTestTag(MarketsTestTags.TOKENS_LIST) },
|
|
||||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
|
||||||
positionMatcher = { position ->
|
|
||||||
SemanticsMatcher.expectValue(
|
|
||||||
LazyListItemPositionSemantics,
|
|
||||||
position
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val addToPortfolioButton: KNode = child {
|
val addToPortfolioButton: KNode = child {
|
||||||
hasTestTag(BaseButtonTestTags.TEXT)
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
hasText(getResourceString(R.string.common_add_to_portfolio))
|
hasText(getResourceString(R.string.common_add_to_portfolio))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val mainNetworkSwitch: KNode = child<KNode> {
|
val mainNetworkSuffix: KNode = child {
|
||||||
hasAnyDescendant(withText(MARKETS_MAIN_NETWORK_SUFFIX))
|
hasText(MARKETS_MAIN_NETWORK_SUFFIX)
|
||||||
useUnmergedTree = true
|
}
|
||||||
}.child { hasTestTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH) }
|
|
||||||
|
|
||||||
val topBarBackButton: KNode = child {
|
val topBarBackButton: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val searchThroughMarketPlaceholder: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.markets_search_header_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
fun tokenWithTitle(title: String): KNode {
|
fun tokenWithTitle(title: String): KNode {
|
||||||
return lazyList.child<KNode> {
|
return child {
|
||||||
|
hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM)
|
||||||
hasText(title)
|
hasText(title)
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
||||||
|
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||||
|
import com.tangem.features.onramp.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 androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
|
class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<MarketsTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val swapPortfolioQuickActionButton: KNode = child {
|
||||||
|
hasTestTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON)
|
||||||
|
hasText(getResourceString(R.string.common_swap), substring = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tokenWithTitle(title: String): KNode = child {
|
||||||
|
hasAnyAncestor(withTestTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_TOKEN_ITEM))
|
||||||
|
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||||
|
hasAnySibling(withTestTag(TokenElementsTestTags.TOKEN_ICON))
|
||||||
|
hasAnyChild(withText(title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onMarketsTokenDetailsScreen(function: MarketsTokenDetailsPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
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.SearchBarTestTags
|
||||||
|
import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags
|
||||||
|
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||||
|
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 androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
|
class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<SwapSelectTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val title: KNode = child {
|
||||||
|
hasTestTag(TopAppBarTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.common_swap))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val closeButton: KNode = child {
|
||||||
|
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||||
|
}
|
||||||
|
|
||||||
|
val youSwapTitle: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.swapping_from_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val youSwapBlock: KNode = child {
|
||||||
|
hasTestTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK)
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.action_buttons_you_want_to_swap)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val youReceiveTitle: KNode = child {
|
||||||
|
hasText(getResourceString(R.string.swapping_to_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val youReceiveBlock: KNode = child {
|
||||||
|
hasTestTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK)
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.action_buttons_you_want_to_receive)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchBarIcon: KNode = child {
|
||||||
|
hasTestTag(SearchBarTestTags.ICON)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchBarPlaceholderText: KNode = child {
|
||||||
|
hasTestTag(SearchBarTestTags.PLACEHOLDER_TEXT)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tokenWithName(tokenName: String): KNode = child {
|
||||||
|
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
|
||||||
|
hasAnyChild(withText(tokenName))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -10,10 +10,31 @@ import io.github.kakaocup.compose.node.element.KNode
|
||||||
class SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<SwapStoriesPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<SwapStoriesPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val container: KNode = child {
|
||||||
|
hasTestTag(SwapStoriesScreenTestTags.SCREEN_CONTAINER)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
val closeButton: KNode = child {
|
val closeButton: KNode = child {
|
||||||
hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON)
|
hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun progressBarItem(index: Int): KNode = child {
|
||||||
|
hasTestTag(SwapStoriesScreenTestTags.PROGRESS_BAR_ITEM)
|
||||||
|
hasPosition(index)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val title: KNode = child {
|
||||||
|
hasTestTag(SwapStoriesScreenTestTags.TITLE)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val subtitle: KNode = child {
|
||||||
|
hasTestTag(SwapStoriesScreenTestTags.SUBTITLE)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) =
|
internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) =
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<SwapTokenPageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<SwapTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val container: KNode = child {
|
||||||
|
hasTestTag(SwapTokenScreenTestTags.CONTAINER)
|
||||||
|
}
|
||||||
|
|
||||||
val title: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.TITLE)
|
hasTestTag(TopAppBarTestTags.TITLE)
|
||||||
hasText(getResourceString(R.string.common_swap))
|
hasText(getResourceString(R.string.common_swap))
|
||||||
|
|
@ -78,6 +82,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
||||||
|
|
||||||
fun tokenSymbol(symbol: String): KNode = child {
|
fun tokenSymbol(symbol: String): KNode = child {
|
||||||
hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL)
|
hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL)
|
||||||
|
hasText(symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tests.actionButtons
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||||
import com.tangem.common.extensions.SwipeDirection
|
import com.tangem.common.extensions.SwipeDirection
|
||||||
|
import com.tangem.common.extensions.assertIsDimmed
|
||||||
import com.tangem.common.extensions.clickWithAssertion
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
import com.tangem.common.extensions.swipeVertical
|
import com.tangem.common.extensions.swipeVertical
|
||||||
import com.tangem.common.utils.resetWireMockScenarioState
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
|
@ -60,8 +61,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
@Test
|
@Test
|
||||||
fun checkActionButtonsStateTest() {
|
fun checkActionButtonsStateTest() {
|
||||||
val tokenTitle = "Bitcoin"
|
val tokenTitle = "Bitcoin"
|
||||||
val actionButtonIsNotDimmed = "Action button is not dimmed"
|
|
||||||
val actionButtonIsDimmed = "Action button is dimmed"
|
|
||||||
|
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
|
|
@ -75,19 +74,19 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Receive' button is not dimmed") {
|
step("Assert 'Receive' button is not dimmed") {
|
||||||
onTokenDetailsScreen { receiveButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
|
onTokenDetailsScreen { receiveButton().assertIsDimmed(false) }
|
||||||
}
|
}
|
||||||
step("Assert 'Buy' button is not dimmed") {
|
step("Assert 'Buy' button is not dimmed") {
|
||||||
onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
|
onTokenDetailsScreen { buyButton().assertIsDimmed(false) }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is not dimmed") {
|
step("Assert 'Send' button is not dimmed") {
|
||||||
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
|
onTokenDetailsScreen { sendButton().assertIsDimmed(false) }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Sell' button is dimmed") {
|
step("Assert 'Sell' button is dimmed") {
|
||||||
onTokenDetailsScreen { sellButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { sellButton().assertIsDimmed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +129,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
@Test
|
@Test
|
||||||
fun checkSwapButtonProviderErrorTest() {
|
fun checkSwapButtonProviderErrorTest() {
|
||||||
val tokenTitle = "POL (ex-MATIC)"
|
val tokenTitle = "POL (ex-MATIC)"
|
||||||
val actionButtonIsDimmed = "Action button is dimmed"
|
|
||||||
|
|
||||||
setupHooks().run {
|
setupHooks().run {
|
||||||
step("Open 'Main Screen'") {
|
step("Open 'Main Screen'") {
|
||||||
|
|
@ -147,7 +145,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton().performClick() }
|
||||||
|
|
@ -162,7 +160,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onSwapIsNotSupportedDialog { okButton.performClick() }
|
onSwapIsNotSupportedDialog { okButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +170,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
@Test
|
@Test
|
||||||
fun checkSwapButtonExpressErrorTest() {
|
fun checkSwapButtonExpressErrorTest() {
|
||||||
val tokenTitle = "Polygon"
|
val tokenTitle = "Polygon"
|
||||||
val actionButtonIsDimmed = "Action button is dimmed"
|
|
||||||
val scenarioName = "express_api_assets"
|
val scenarioName = "express_api_assets"
|
||||||
val scenarioState = "Error"
|
val scenarioState = "Error"
|
||||||
|
|
||||||
|
|
@ -198,7 +195,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
onTokenDetailsScreen { swapButton().performClick() }
|
onTokenDetailsScreen { swapButton().performClick() }
|
||||||
|
|
@ -213,7 +210,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
onOperationIsUnavailableDialog { okButton.performClick() }
|
onOperationIsUnavailableDialog { okButton.performClick() }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { swapButton().assertIsDimmed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
||||||
val tokenTitle = "XRP"
|
val tokenTitle = "XRP"
|
||||||
val scenarioName = "quotes_api"
|
val scenarioName = "quotes_api"
|
||||||
val scenarioState = "Ripple"
|
val scenarioState = "Ripple"
|
||||||
val updatedBalance = "$3,307.18"
|
val updatedBalance = "$3,320.47"
|
||||||
setupHooks(
|
setupHooks(
|
||||||
additionalAfterSection = {
|
additionalAfterSection = {
|
||||||
resetWireMockScenarioState(scenarioName)
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
|
@ -85,14 +85,18 @@ class TotalBalanceUpdateTest : BaseTestCase() {
|
||||||
step("Click on 'Add to portfolio' button") {
|
step("Click on 'Add to portfolio' button") {
|
||||||
onMarketsScreen { addToPortfolioButton.clickWithAssertion() }
|
onMarketsScreen { addToPortfolioButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Toggle the main network switch") {
|
step("Click on main network") {
|
||||||
onMarketsScreen { mainNetworkSwitch.performClick() }
|
onMarketsScreen { mainNetworkSuffix.performClick() }
|
||||||
}
|
}
|
||||||
step("Click on 'Continue' button") {
|
step("Click on 'Add' button") {
|
||||||
onDialog { continueButton.clickWithAssertion() }
|
onDialog { addButton.clickWithAssertion() }
|
||||||
|
onDialog { addButton.clickWithAssertion() } //TODO: [REDACTED_JIRA]
|
||||||
}
|
}
|
||||||
step("Assert 'Continue' is not displayed") {
|
step("Assert 'Continue' is not displayed") {
|
||||||
onDialog { continueButton.assertIsNotDisplayed() }
|
onDialog { addButton.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Later' button") {
|
||||||
|
onDialog { laterButton.clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Go back to 'Markets: tokens list'") {
|
step("Go back to 'Markets: tokens list'") {
|
||||||
waitForIdle()
|
waitForIdle()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.tangem.tests.swap
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.scenarios.openMainScreen
|
||||||
|
import com.tangem.scenarios.synchronizeAddresses
|
||||||
|
import com.tangem.screens.onMainScreen
|
||||||
|
import com.tangem.screens.onSwapSelectTokenScreen
|
||||||
|
import com.tangem.screens.onSwapStoriesScreen
|
||||||
|
import com.tangem.screens.onSwapTokenScreen
|
||||||
|
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 SwapSelectTokenScreenTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("2829")
|
||||||
|
@DisplayName("Open 'Swap select token' screen from 'Main' screen")
|
||||||
|
@Test
|
||||||
|
fun openSwapSelectTokenScreenFromMainScreenTest() {
|
||||||
|
val swapTokenName = "Ethereum"
|
||||||
|
val receiveTokenName = "Polygon"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on 'Swap' button") {
|
||||||
|
onMainScreen { swapButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Close 'Stories' screen") {
|
||||||
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap select token' screen title is displayed") {
|
||||||
|
onSwapSelectTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'You swap' title is displayed") {
|
||||||
|
onSwapSelectTokenScreen { youSwapTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'You swap' block is displayed") {
|
||||||
|
onSwapSelectTokenScreen { youSwapBlock.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert search icon is displayed") {
|
||||||
|
onSwapSelectTokenScreen { searchBarIcon.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert search placeholder is displayed") {
|
||||||
|
onSwapSelectTokenScreen { searchBarPlaceholderText.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on token with name '$swapTokenName'") {
|
||||||
|
onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'You receive' title is displayed") {
|
||||||
|
onSwapSelectTokenScreen { youReceiveTitle.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'You receive' block is displayed") {
|
||||||
|
onSwapSelectTokenScreen { youReceiveBlock.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on token with name '$receiveTokenName'") {
|
||||||
|
onSwapSelectTokenScreen { tokenWithName(receiveTokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap token' screen is opened") {
|
||||||
|
onSwapTokenScreen { container.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,437 @@
|
||||||
|
package com.tangem.tests.swap
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.longClick
|
||||||
|
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.extensions.assertHasBadge
|
||||||
|
import com.tangem.common.extensions.restartApp
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
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 SwapStoriesTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("5453")
|
||||||
|
@DisplayName("Check 'Swap' button badge on 'Main' screen")
|
||||||
|
@Test
|
||||||
|
fun checkMainScreenSwapButtonBadgeTest() {
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onMainScreen { swapButton.assertHasBadge() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MainScreen)
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has not badge") {
|
||||||
|
onMainScreen { swapButton.assertHasBadge(false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5454")
|
||||||
|
@DisplayName("Check 'Swap' button badge on token details screen")
|
||||||
|
@Test
|
||||||
|
fun checkTokenDetailsScreenSwapButtonTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onTokenDetailsScreen { swapButton().assertHasBadge() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenDetails)
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has not badge") {
|
||||||
|
onTokenDetailsScreen { swapButton().assertHasBadge(false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5455")
|
||||||
|
@DisplayName("Check 'Swap' button badge on token details in 'Market' screen")
|
||||||
|
@Test
|
||||||
|
fun checkMarketTokenDetailsScreenSwapButtonTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
val badgeShown = "Badge shown"
|
||||||
|
val badgeHidden = "Badge hidden"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Open 'Markets' token details screen for token '$tokenName'") {
|
||||||
|
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails)
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has not badge") {
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5469")
|
||||||
|
@DisplayName("Check unavailable swap stories on 'Main' screen")
|
||||||
|
@Test
|
||||||
|
fun checkUnavailableSwapStoriesOnMainScreen() {
|
||||||
|
val scenarioName = "stories_first_time_swap"
|
||||||
|
val scenarioErrorState = "Error"
|
||||||
|
val packageName = getTargetContext().packageName
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioErrorState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onMainScreen { swapButton.assertHasBadge(false) }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false)
|
||||||
|
}
|
||||||
|
step("Reset WireMock scenario state") {
|
||||||
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Restart app") {
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button is displayed") {
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen { swapButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onMainScreen { swapButton.assertHasBadge() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5471")
|
||||||
|
@DisplayName("Check unavailable swap stories on 'Token details' screen")
|
||||||
|
@Test
|
||||||
|
fun checkUnavailableSwapStoriesOnTokenDetailsScreen() {
|
||||||
|
val scenarioName = "stories_first_time_swap"
|
||||||
|
val scenarioErrorState = "Error"
|
||||||
|
val packageName = getTargetContext().packageName
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioErrorState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onTokenDetailsScreen { swapButton().assertHasBadge(false) }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Reset WireMock scenario state") {
|
||||||
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
}
|
||||||
|
step("Restart app") {
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen { swapButton.assertHasBadge() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5471")
|
||||||
|
@DisplayName("Check unavailable swap stories on 'Markets' token details screen")
|
||||||
|
@Test
|
||||||
|
fun checkUnavailableSwapStoriesOnMarketsTokenDetailsScreen() {
|
||||||
|
val scenarioName = "stories_first_time_swap"
|
||||||
|
val scenarioErrorState = "Error"
|
||||||
|
val packageName = getTargetContext().packageName
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
val badgeShown = "Badge shown"
|
||||||
|
val badgeHidden = "Badge hidden"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalBeforeAppLaunchSection = {
|
||||||
|
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioErrorState)
|
||||||
|
},
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Open 'Markets' token details screen for token '$tokenName'") {
|
||||||
|
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
waitForIdle()
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false)
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Reset WireMock scenario state") {
|
||||||
|
resetWireMockScenarioState(scenarioName)
|
||||||
|
}
|
||||||
|
step("Restart app") {
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
step("Open 'Markets' token details screen for token '$tokenName'") {
|
||||||
|
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
waitForIdle()
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() }
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5474")
|
||||||
|
@DisplayName("Check 'Swap' stories on 'Main' screen")
|
||||||
|
@Test
|
||||||
|
fun checkSwapStoriesOnMainScreenTest() {
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on 'Swap' button on 'Main' screen") {
|
||||||
|
onMainScreen { swapButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Check stories changes") {
|
||||||
|
checkStoriesChanges()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapStoriesScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' screen title is displayed") {
|
||||||
|
onSwapTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen without stories") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5475")
|
||||||
|
@DisplayName("Check 'Swap' stories on 'Token details' screen")
|
||||||
|
@Test
|
||||||
|
fun checkSwapStoriesOnTokenDetailsScreenTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenName'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' button has badge") {
|
||||||
|
onTokenDetailsScreen { swapButton().assertHasBadge() }
|
||||||
|
}
|
||||||
|
step("Click on 'Swap' button on 'Token details' screen") {
|
||||||
|
onTokenDetailsScreen { swapButton().performClick() }
|
||||||
|
}
|
||||||
|
step("Check stories changes") {
|
||||||
|
checkStoriesChanges()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapStoriesScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' screen title is displayed") {
|
||||||
|
onSwapTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen without stories") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5476")
|
||||||
|
@DisplayName("Check 'Swap' stories on 'Markets' screen")
|
||||||
|
@Test
|
||||||
|
fun checkSwapStoriesOnMarketTokenDetailsScreenTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Open 'Markets' token details screen for token '$tokenName'") {
|
||||||
|
openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName)
|
||||||
|
}
|
||||||
|
step("Click on 'Swap' button on 'Markets' token details screen") {
|
||||||
|
onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Check stories changes") {
|
||||||
|
checkStoriesChanges()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapStoriesScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' screen title is displayed") {
|
||||||
|
onSwapTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen without stories") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("5477")
|
||||||
|
@DisplayName("Check 'Swap' stories on token actions bottom sheet")
|
||||||
|
@Test
|
||||||
|
fun checkSwapStoriesOnTokenActionsBottomSheetTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Long click on token with name: '$tokenName'") {
|
||||||
|
waitForIdle()
|
||||||
|
onMainScreen {
|
||||||
|
tokenWithTitleAndAddress(tokenName).performTouchInput {
|
||||||
|
longClick(
|
||||||
|
position = center,
|
||||||
|
durationMillis = 1000L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Swap' button") {
|
||||||
|
onTokenActionsBottomSheet { swapButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Check stories changes") {
|
||||||
|
checkStoriesChanges()
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapStoriesScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' screen title is displayed") {
|
||||||
|
onSwapTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Close' button") {
|
||||||
|
onSwapTokenScreen { closeButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Open 'Swap' screen without stories") {
|
||||||
|
openSwapScreen(from = SwapEntryPoint.TokenActionsBottomSheet, storiesExist = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.tests
|
package com.tangem.tests.swap
|
||||||
|
|
||||||
import androidx.compose.ui.test.hasText
|
import androidx.compose.ui.test.hasText
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
|
|
@ -19,7 +19,7 @@ import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@HiltAndroidTest
|
@HiltAndroidTest
|
||||||
class SwapTokenTest : BaseTestCase() {
|
class SwapTokenScreenTest : BaseTestCase() {
|
||||||
|
|
||||||
@ApiEnv(
|
@ApiEnv(
|
||||||
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
|
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
|
||||||
|
|
@ -43,7 +43,7 @@ class SwapTokenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Assert title: '$tokenTitle' is displayed") {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
|
|
@ -123,7 +123,7 @@ class SwapTokenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Assert title: '$tokenTitle' is displayed") {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Turn off Wi-Fi and Mobile Data") {
|
step("Turn off Wi-Fi and Mobile Data") {
|
||||||
|
|
@ -171,7 +171,7 @@ class SwapTokenTest : BaseTestCase() {
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||||
}
|
}
|
||||||
step("Click on token with name: '$tokenTitle'") {
|
step("Assert title: '$tokenTitle' is displayed") {
|
||||||
onTokenDetailsScreen { title.assertIsDisplayed() }
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Swap' button") {
|
step("Click on 'Swap' button") {
|
||||||
|
|
@ -239,4 +239,55 @@ class SwapTokenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllureId("2828")
|
||||||
|
@DisplayName("Swap: network fee")
|
||||||
|
@Test
|
||||||
|
fun goToTokenSwapTest() {
|
||||||
|
val swapTokenSymbol = "POL"
|
||||||
|
val receiveTokenSymbol = "ETH"
|
||||||
|
val tokenTitle = "Polygon"
|
||||||
|
|
||||||
|
setupHooks().run {
|
||||||
|
|
||||||
|
resetWireMockScenarios()
|
||||||
|
step("Open 'Main Screen'") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Click on token with name: '$tokenTitle'") {
|
||||||
|
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert title: '$tokenTitle' is displayed") {
|
||||||
|
onTokenDetailsScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Swap' button") {
|
||||||
|
onTokenDetailsScreen { swapButton().performClick() }
|
||||||
|
}
|
||||||
|
step("Close 'Stories' screen") {
|
||||||
|
onSwapStoriesScreen { closeButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap' screen title is displayed") {
|
||||||
|
onSwapTokenScreen { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Close' button is displayed") {
|
||||||
|
onSwapTokenScreen { closeButton.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Swap tokens on screen' button is displayed") {
|
||||||
|
onSwapTokenScreen {
|
||||||
|
flakySafely(WAIT_UNTIL_TIMEOUT) {
|
||||||
|
swapTokensOnscreenButton.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert token symbol: '$swapTokenSymbol' is displayed") {
|
||||||
|
onSwapTokenScreen { tokenSymbol(swapTokenSymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert token symbol: '$receiveTokenSymbol' is displayed") {
|
||||||
|
onSwapTokenScreen { tokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
50
app/src/google/java/com/tangem/tap/GoogleReviewManager.kt
Normal file
50
app/src/google/java/com/tangem/tap/GoogleReviewManager.kt
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.tangem.tap
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import com.google.android.gms.tasks.Task
|
||||||
|
import com.google.android.play.core.review.ReviewInfo
|
||||||
|
import com.google.android.play.core.review.ReviewManagerFactory
|
||||||
|
import com.tangem.core.navigation.review.ReviewManager
|
||||||
|
import timber.log.Timber
|
||||||
|
import com.google.android.play.core.review.ReviewManager as GReviewManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of [ReviewManager] for Google Play Store.
|
||||||
|
*/
|
||||||
|
internal class GoogleReviewManager : ReviewManager {
|
||||||
|
|
||||||
|
override fun request(onDismissClick: () -> Unit) {
|
||||||
|
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||||
|
val reviewManager = ReviewManagerFactory.create(activity)
|
||||||
|
val requestTask = reviewManager.requestReviewFlow()
|
||||||
|
requestTask
|
||||||
|
.addOnCompleteListener { task ->
|
||||||
|
handleOnCompleteRequestTask(
|
||||||
|
reviewManager = reviewManager,
|
||||||
|
activity = activity,
|
||||||
|
task = task,
|
||||||
|
onDismissClick = onDismissClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.addOnFailureListener(Timber::e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleOnCompleteRequestTask(
|
||||||
|
reviewManager: GReviewManager,
|
||||||
|
activity: Activity,
|
||||||
|
task: Task<ReviewInfo>,
|
||||||
|
onDismissClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
if (task.isSuccessful) {
|
||||||
|
val reviewFlow = reviewManager.launchReviewFlow(activity, task.result)
|
||||||
|
reviewFlow
|
||||||
|
.addOnCompleteListener { resultReviewTask ->
|
||||||
|
if (!resultReviewTask.isSuccessful) onDismissClick()
|
||||||
|
}
|
||||||
|
.addOnFailureListener(Timber::e)
|
||||||
|
} else {
|
||||||
|
Timber.e(task.exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.tap.di
|
||||||
|
|
||||||
|
import com.tangem.core.navigation.review.ReviewManager
|
||||||
|
import com.tangem.tap.GoogleReviewManager
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal class GoogleReviewManagerModule {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
fun provideGoogleReviewManager(): ReviewManager {
|
||||||
|
return GoogleReviewManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.tap.di
|
||||||
|
|
||||||
|
import com.tangem.core.navigation.review.DummyReviewManager
|
||||||
|
import com.tangem.core.navigation.review.ReviewManager
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal class HuaweiReviewManagerModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideHuaweiReviewManager(): ReviewManager {
|
||||||
|
return DummyReviewManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -40,9 +40,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
|
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.hot.sdk.TangemHotSdk
|
import com.tangem.hot.sdk.TangemHotSdk
|
||||||
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
|
||||||
|
|
@ -92,8 +90,6 @@ interface ApplicationEntryPoint {
|
||||||
|
|
||||||
fun getOneTimeEventFilter(): OneTimeEventFilter
|
fun getOneTimeEventFilter(): OneTimeEventFilter
|
||||||
|
|
||||||
fun getGeneralUserWalletsListManager(): UserWalletsListManager
|
|
||||||
|
|
||||||
fun getWasTwinsOnboardingShownUseCase(): WasTwinsOnboardingShownUseCase
|
fun getWasTwinsOnboardingShownUseCase(): WasTwinsOnboardingShownUseCase
|
||||||
|
|
||||||
fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase
|
fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase
|
||||||
|
|
@ -151,8 +147,6 @@ interface ApplicationEntryPoint {
|
||||||
|
|
||||||
fun getTangemHotSdk(): TangemHotSdk
|
fun getTangemHotSdk(): TangemHotSdk
|
||||||
|
|
||||||
fun getHotWalletFeatureToggles(): HotWalletFeatureToggles
|
|
||||||
|
|
||||||
fun getWcInitializeUseCase(): WcInitializeUseCase
|
fun getWcInitializeUseCase(): WcInitializeUseCase
|
||||||
|
|
||||||
fun getTrackingContextProxy(): TrackingContextProxy
|
fun getTrackingContextProxy(): TrackingContextProxy
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,6 @@ import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.legacy.asLockable
|
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -18,21 +15,12 @@ class LockTimerWorker @AssistedInject constructor(
|
||||||
@Assisted context: Context,
|
@Assisted context: Context,
|
||||||
@Assisted params: WorkerParameters,
|
@Assisted params: WorkerParameters,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.i("onStart job")
|
Timber.i("onStart job")
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
userWalletsListRepository.lockAllWallets().onRight {
|
||||||
userWalletsListRepository.lockAllWallets()
|
|
||||||
.onRight {
|
|
||||||
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure()
|
|
||||||
userWalletsListManagerLockable.lock()
|
|
||||||
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
|
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
|
||||||
}
|
}
|
||||||
Timber.i("onStart job complete")
|
Timber.i("onStart job complete")
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@ import androidx.work.WorkManager
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
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.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.tap.LockTimerWorker.Companion.TAG
|
import com.tangem.tap.LockTimerWorker.Companion.TAG
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -22,16 +19,13 @@ import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class LockUserWalletsTimer(
|
internal class LockUserWalletsTimer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
private val duration: Duration = with(Duration) { 5.minutes },
|
private val duration: Duration = with(Duration) { 5.minutes },
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
|
private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase,
|
||||||
) : LifecycleOwner by context as LifecycleOwner,
|
) : LifecycleOwner by context as LifecycleOwner,
|
||||||
|
|
@ -59,9 +53,7 @@ internal class LockUserWalletsTimer(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (shouldOpenWelcomeScreenOnResume) {
|
if (shouldOpenWelcomeScreenOnResume) {
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
clearAllHotWalletContextualUnlockUseCase.invoke()
|
||||||
clearAllHotWalletContextualUnlockUseCase.invoke()
|
|
||||||
}
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
||||||
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
|
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
|
||||||
}
|
}
|
||||||
|
|
@ -115,38 +107,18 @@ internal class LockUserWalletsTimer(
|
||||||
* This job used only when app is foreground when background use [JobScheduler]
|
* This job used only when app is foreground when background use [JobScheduler]
|
||||||
*/
|
*/
|
||||||
private fun createDelayJob(): Job = coroutineScope.launch {
|
private fun createDelayJob(): Job = coroutineScope.launch {
|
||||||
val startTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
delay(duration)
|
delay(duration)
|
||||||
|
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
val userWallets = userWalletsListRepository.userWalletsSync()
|
||||||
val userWallets = userWalletsListRepository.userWalletsSync()
|
if (userWallets.isNotEmpty()) {
|
||||||
if (userWallets.isNotEmpty()) {
|
userWalletsListRepository.lockAllWallets()
|
||||||
userWalletsListRepository.lockAllWallets()
|
.onLeft {
|
||||||
.onLeft {
|
start()
|
||||||
start()
|
}
|
||||||
}
|
.onRight {
|
||||||
.onRight {
|
clearAllHotWalletContextualUnlockUseCase.invoke()
|
||||||
clearAllHotWalletContextualUnlockUseCase.invoke()
|
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
|
|
||||||
|
|
||||||
if (userWalletsListManager.hasUserWallets) {
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
|
|
||||||
Timber.i(
|
|
||||||
"""
|
|
||||||
Finished
|
|
||||||
|- Millis passed: ${currentTime - startTime}
|
|
||||||
""".trimIndent(),
|
|
||||||
)
|
|
||||||
|
|
||||||
userWalletsListManager.lock()
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,9 +43,7 @@ import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
|
||||||
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
|
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
|
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||||
import com.tangem.features.tester.api.TesterMenuLauncher
|
import com.tangem.features.tester.api.TesterMenuLauncher
|
||||||
import com.tangem.google.GoogleServicesHelper
|
import com.tangem.google.GoogleServicesHelper
|
||||||
|
|
@ -114,9 +112,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var analyticsEventsHandler: AnalyticsEventHandler
|
lateinit var analyticsEventsHandler: AnalyticsEventHandler
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var userWalletsListManager: UserWalletsListManager
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@RootAppComponentContext
|
@RootAppComponentContext
|
||||||
internal lateinit var rootComponentContext: AppComponentContext
|
internal lateinit var rootComponentContext: AppComponentContext
|
||||||
|
|
@ -163,9 +158,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
@Inject
|
@Inject
|
||||||
internal lateinit var userWalletsListRepository: UserWalletsListRepository
|
internal lateinit var userWalletsListRepository: UserWalletsListRepository
|
||||||
|
|
||||||
@Inject
|
|
||||||
internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
|
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
|
||||||
|
|
||||||
|
|
@ -258,10 +250,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
||||||
lockUserWalletsTimer = LockUserWalletsTimer(
|
lockUserWalletsTimer = LockUserWalletsTimer(
|
||||||
context = this,
|
context = this,
|
||||||
settingsRepository = settingsRepository,
|
settingsRepository = settingsRepository,
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
coroutineScope = mainScope,
|
coroutineScope = mainScope,
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
hotWalletFeatureToggles = hotWalletFeatureToggles,
|
|
||||||
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
|
clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
import com.tangem.operations.attestation.api.TangemApiServiceSettings
|
||||||
|
|
@ -146,9 +145,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
private val oneTimeEventFilter: OneTimeEventFilter
|
private val oneTimeEventFilter: OneTimeEventFilter
|
||||||
get() = entryPoint.getOneTimeEventFilter()
|
get() = entryPoint.getOneTimeEventFilter()
|
||||||
|
|
||||||
private val generalUserWalletsListManager: UserWalletsListManager
|
|
||||||
get() = entryPoint.getGeneralUserWalletsListManager()
|
|
||||||
|
|
||||||
private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
|
private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
|
||||||
get() = entryPoint.getWasTwinsOnboardingShownUseCase()
|
get() = entryPoint.getWasTwinsOnboardingShownUseCase()
|
||||||
|
|
||||||
|
|
@ -238,9 +234,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
private val tangemHotSdk
|
private val tangemHotSdk
|
||||||
get() = entryPoint.getTangemHotSdk()
|
get() = entryPoint.getTangemHotSdk()
|
||||||
|
|
||||||
private val hotWalletFeatureToggles
|
|
||||||
get() = entryPoint.getHotWalletFeatureToggles()
|
|
||||||
|
|
||||||
private val wcInitializeUseCase
|
private val wcInitializeUseCase
|
||||||
get() = entryPoint.getWcInitializeUseCase()
|
get() = entryPoint.getWcInitializeUseCase()
|
||||||
|
|
||||||
|
|
@ -382,7 +375,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
appThemeModeRepository = appThemeModeRepository,
|
appThemeModeRepository = appThemeModeRepository,
|
||||||
balanceHidingRepository = balanceHidingRepository,
|
balanceHidingRepository = balanceHidingRepository,
|
||||||
walletsRepository = walletsRepository,
|
walletsRepository = walletsRepository,
|
||||||
generalUserWalletsListManager = generalUserWalletsListManager,
|
|
||||||
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
|
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
|
||||||
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
|
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
|
||||||
cardRepository = cardRepository,
|
cardRepository = cardRepository,
|
||||||
|
|
@ -407,7 +399,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
|
||||||
userTokensResponseStore = userTokensResponseStore,
|
userTokensResponseStore = userTokensResponseStore,
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
tangemHotSdk = tangemHotSdk,
|
tangemHotSdk = tangemHotSdk,
|
||||||
hotWalletFeatureToggles = hotWalletFeatureToggles,
|
|
||||||
trackingContextProxy = trackingContextProxy,
|
trackingContextProxy = trackingContextProxy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.common.redux
|
||||||
|
|
||||||
import com.tangem.tap.common.redux.global.globalReducer
|
import com.tangem.tap.common.redux.global.globalReducer
|
||||||
import com.tangem.tap.features.details.redux.DetailsReducer
|
import com.tangem.tap.features.details.redux.DetailsReducer
|
||||||
import com.tangem.tap.features.welcome.redux.WelcomeReducer
|
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphReducer
|
import com.tangem.tap.proxy.redux.DaggerGraphReducer
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
||||||
|
|
@ -12,7 +11,6 @@ fun appReducer(action: Action, state: AppState): AppState {
|
||||||
return AppState(
|
return AppState(
|
||||||
globalState = globalReducer(action, state),
|
globalState = globalReducer(action, state),
|
||||||
detailsState = DetailsReducer.reduce(action, state),
|
detailsState = DetailsReducer.reduce(action, state),
|
||||||
welcomeState = WelcomeReducer.reduce(action, state),
|
|
||||||
daggerGraphState = DaggerGraphReducer.reduce(action, state),
|
daggerGraphState = DaggerGraphReducer.reduce(action, state),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
||||||
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
|
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
|
||||||
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
|
|
||||||
import com.tangem.tap.features.welcome.redux.WelcomeState
|
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import org.rekotlin.Middleware
|
import org.rekotlin.Middleware
|
||||||
|
|
@ -17,7 +15,6 @@ import org.rekotlin.StateType
|
||||||
data class AppState(
|
data class AppState(
|
||||||
val globalState: GlobalState = GlobalState(),
|
val globalState: GlobalState = GlobalState(),
|
||||||
val detailsState: DetailsState = DetailsState(),
|
val detailsState: DetailsState = DetailsState(),
|
||||||
val welcomeState: WelcomeState = WelcomeState(),
|
|
||||||
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
|
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
|
||||||
) : StateType {
|
) : StateType {
|
||||||
|
|
||||||
|
|
@ -28,7 +25,6 @@ data class AppState(
|
||||||
GlobalMiddleware.handler,
|
GlobalMiddleware.handler,
|
||||||
DetailsMiddleware().detailsMiddleware,
|
DetailsMiddleware().detailsMiddleware,
|
||||||
BackupMiddleware().backupMiddleware,
|
BackupMiddleware().backupMiddleware,
|
||||||
WelcomeMiddleware().middleware,
|
|
||||||
LockUserWalletsTimerMiddleware().middleware,
|
LockUserWalletsTimerMiddleware().middleware,
|
||||||
AccessCodeRequestPolicyMiddleware().middleware,
|
AccessCodeRequestPolicyMiddleware().middleware,
|
||||||
DaggerGraphMiddleware.daggerGraphMiddleware,
|
DaggerGraphMiddleware.daggerGraphMiddleware,
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ internal object LegacyMiddleware {
|
||||||
{ action ->
|
{ action ->
|
||||||
when (action) {
|
when (action) {
|
||||||
is LegacyAction.PrepareDetailsScreen -> {
|
is LegacyAction.PrepareDetailsScreen -> {
|
||||||
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
|
|
||||||
|
|
||||||
selectedUserWallet()
|
selectedUserWallet()
|
||||||
.distinctUntilChanged { old, new ->
|
.distinctUntilChanged { old, new ->
|
||||||
if (old is UserWallet.Cold && new is UserWallet.Cold) {
|
if (old is UserWallet.Cold && new is UserWallet.Cold) {
|
||||||
|
|
@ -39,9 +37,7 @@ internal object LegacyMiddleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onEach { selectedUserWallet ->
|
.onEach { selectedUserWallet ->
|
||||||
val initializedAppSettingsStateContent = initializeAppSettingsState(
|
val initializedAppSettingsStateContent = initializeAppSettingsState()
|
||||||
shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(),
|
|
||||||
)
|
|
||||||
store.dispatchWithMain(
|
store.dispatchWithMain(
|
||||||
DetailsAction.PrepareScreen(
|
DetailsAction.PrepareScreen(
|
||||||
scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse,
|
scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse,
|
||||||
|
|
@ -60,24 +56,15 @@ internal object LegacyMiddleware {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun selectedUserWallet(): Flow<UserWallet> {
|
private fun selectedUserWallet(): Flow<UserWallet> {
|
||||||
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
|
return store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull()
|
||||||
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
|
* LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking
|
||||||
* previously it was initialized in runBlocking and blocked details screen
|
* previously it was initialized in runBlocking and blocked details screen
|
||||||
*/
|
*/
|
||||||
private suspend fun initializeAppSettingsState(shouldSaveUserWallets: Boolean): AppSettingsState {
|
private suspend fun initializeAppSettingsState(): AppSettingsState {
|
||||||
return AppSettingsState(
|
return AppSettingsState(
|
||||||
isBiometricsAvailable = tangemSdkManager.checkCanUseBiometry(),
|
|
||||||
saveWallets = shouldSaveUserWallets,
|
|
||||||
saveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes(),
|
|
||||||
selectedAppCurrency = store.state.globalState.appCurrency,
|
selectedAppCurrency = store.state.globalState.appCurrency,
|
||||||
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
|
selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull()
|
||||||
?: AppThemeMode.DEFAULT,
|
?: AppThemeMode.DEFAULT,
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package com.tangem.tap.data
|
|
||||||
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
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.wallets.legacy.UserWalletsListManager
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented
|
|
||||||
// [REDACTED_JIRA]
|
|
||||||
internal class RuntimeUserWalletsStore(
|
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
|
||||||
) : UserWalletsStore {
|
|
||||||
|
|
||||||
override val selectedUserWalletOrNull: UserWallet?
|
|
||||||
get() = userWalletsListManager.selectedUserWalletSync
|
|
||||||
|
|
||||||
override val userWallets: Flow<List<UserWallet>>
|
|
||||||
get() = userWalletsListManager.userWallets
|
|
||||||
|
|
||||||
override val userWalletsSync: List<UserWallet>
|
|
||||||
get() = userWalletsListManager.userWalletsSync
|
|
||||||
|
|
||||||
override fun getSyncOrNull(key: UserWalletId): UserWallet? {
|
|
||||||
return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getSyncStrict(key: UserWalletId): UserWallet {
|
|
||||||
return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(
|
|
||||||
userWalletId: UserWalletId,
|
|
||||||
update: suspend (UserWallet) -> UserWallet,
|
|
||||||
): CompletionResult<UserWallet> {
|
|
||||||
return userWalletsListManager.update(userWalletId, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,6 @@ package com.tangem.tap.di.data
|
||||||
|
|
||||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.tap.data.RuntimeUserWalletsStore
|
|
||||||
import com.tangem.tap.data.UserWalletsStoreRepositoryProxy
|
import com.tangem.tap.data.UserWalletsStoreRepositoryProxy
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -18,15 +15,7 @@ internal object UserWalletsStoreModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideUserWalletsStore(
|
fun provideUserWalletsStore(userWalletsListRepository: UserWalletsListRepository): UserWalletsStore {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return UserWalletsStoreRepositoryProxy(userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): UserWalletsStore {
|
|
||||||
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
|
||||||
UserWalletsStoreRepositoryProxy(userWalletsListRepository)
|
|
||||||
} else {
|
|
||||||
RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9,9 +9,7 @@ import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.demo.models.DemoConfig
|
import com.tangem.domain.demo.models.DemoConfig
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.usecase.*
|
import com.tangem.domain.wallets.usecase.*
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.sdk.api.TangemSdkManager
|
import com.tangem.sdk.api.TangemSdkManager
|
||||||
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
|
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
|
||||||
import com.tangem.tap.domain.card.DefaultResetCardUseCase
|
import com.tangem.tap.domain.card.DefaultResetCardUseCase
|
||||||
|
|
@ -42,16 +40,8 @@ internal object CardDomainModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideIsNeedToBackupUseCase(
|
fun provideIsNeedToBackupUseCase(userWalletsListRepository: UserWalletsListRepository): IsNeedToBackupUseCase {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return IsNeedToBackupUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): IsNeedToBackupUseCase {
|
|
||||||
return IsNeedToBackupUseCase(
|
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ package com.tangem.tap.di.domain
|
||||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
import com.tangem.domain.card.ScanCardProcessor
|
import com.tangem.domain.card.ScanCardProcessor
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
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.CardScanningFeatureToggles
|
||||||
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
|
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
|
||||||
import com.tangem.tap.domain.scanCard.LegacyScanProcessor
|
import com.tangem.tap.domain.scanCard.LegacyScanProcessor
|
||||||
|
|
@ -34,14 +32,8 @@ internal object CardLegacyDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesWalletNameGenerateUseCase(
|
fun providesWalletNameGenerateUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GenerateWalletNameUseCase {
|
): GenerateWalletNameUseCase {
|
||||||
return GenerateWalletNameUseCase(
|
return GenerateWalletNameUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
|
import com.tangem.domain.earn.repository.EarnRepository
|
||||||
|
import com.tangem.domain.earn.usecase.*
|
||||||
|
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object EarnDomainModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideFetchEarnNetworksUseCase(repository: EarnRepository): FetchEarnNetworksUseCase {
|
||||||
|
return FetchEarnNetworksUseCase(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideManageEarnNetworksUseCase(
|
||||||
|
earnRepository: EarnRepository,
|
||||||
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
|
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||||
|
): GetEarnNetworksUseCase {
|
||||||
|
return GetEarnNetworksUseCase(
|
||||||
|
earnRepository = earnRepository,
|
||||||
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideFetchTopEarnTokensUseCase(repository: EarnRepository): FetchTopEarnTokensUseCase {
|
||||||
|
return FetchTopEarnTokensUseCase(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideManageTopEarnTokensUseCase(repository: EarnRepository): GetTopEarnTokensUseCase {
|
||||||
|
return GetTopEarnTokensUseCase(repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideGetEarnTokensBatchFlowUseCase(repository: EarnRepository): GetEarnTokensBatchFlowUseCase {
|
||||||
|
return GetEarnTokensBatchFlowUseCase(repository)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,8 +13,6 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -107,15 +105,11 @@ object MarketsDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideFilterNetworksUseCase(
|
fun provideFilterNetworksUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
excludedBlockchains: ExcludedBlockchains,
|
excludedBlockchains: ExcludedBlockchains,
|
||||||
): FilterAvailableNetworksForWalletUseCase {
|
): FilterAvailableNetworksForWalletUseCase {
|
||||||
return FilterAvailableNetworksForWalletUseCase(
|
return FilterAvailableNetworksForWalletUseCase(
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
shouldUseNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
excludedBlockchains = excludedBlockchains,
|
excludedBlockchains = excludedBlockchains,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate
|
||||||
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
|
import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate
|
||||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||||
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
import com.tangem.domain.wallets.hot.HotWalletAccessor
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
import com.tangem.domain.wallets.usecase.*
|
import com.tangem.domain.wallets.usecase.*
|
||||||
|
|
@ -22,7 +21,6 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
|
||||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase
|
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
|
import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
|
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.operations.attestation.CardArtworksProvider
|
import com.tangem.operations.attestation.CardArtworksProvider
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -38,105 +36,61 @@ internal object WalletsDomainModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesUserWalletsSyncDelegate(
|
fun providesUserWalletsSyncDelegate(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
|
||||||
): UserWalletsSyncDelegate {
|
): UserWalletsSyncDelegate {
|
||||||
return DefaultUserWalletsSyncDelegate(
|
return DefaultUserWalletsSyncDelegate(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
dispatchers = dispatchers,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGetWalletsUseCase(
|
fun providesGetWalletsUseCase(userWalletsListRepository: UserWalletsListRepository): GetWalletsUseCase {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return GetWalletsUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GetWalletsUseCase {
|
|
||||||
return GetWalletsUseCase(
|
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesWalletNameMigrationUseCase(
|
fun providesWalletNameMigrationUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
walletNamesMigrationRepository: WalletNamesMigrationRepository,
|
walletNamesMigrationRepository: WalletNamesMigrationRepository,
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): WalletNameMigrationUseCase {
|
): WalletNameMigrationUseCase {
|
||||||
return WalletNameMigrationUseCase(
|
return WalletNameMigrationUseCase(
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
walletNamesMigrationRepository = walletNamesMigrationRepository,
|
walletNamesMigrationRepository = walletNamesMigrationRepository,
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGetUserWalletUseCase(
|
fun providesGetUserWalletUseCase(userWalletsListRepository: UserWalletsListRepository): GetUserWalletUseCase {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GetUserWalletUseCase {
|
|
||||||
return GetUserWalletUseCase(
|
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGetSelectedWalletSyncUseCase(
|
fun providesGetSelectedWalletSyncUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GetSelectedWalletSyncUseCase {
|
): GetSelectedWalletSyncUseCase {
|
||||||
return GetSelectedWalletSyncUseCase(
|
return GetSelectedWalletSyncUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGetSelectedWalletUseCase(
|
fun providesGetSelectedWalletUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GetSelectedWalletUseCase {
|
): GetSelectedWalletUseCase {
|
||||||
return GetSelectedWalletUseCase(
|
return GetSelectedWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesSaveWalletUseCase(
|
fun providesSaveWalletUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
walletsRepository: WalletsRepository,
|
walletsRepository: WalletsRepository,
|
||||||
analyticsEventHandler: AnalyticsEventHandler,
|
analyticsEventHandler: AnalyticsEventHandler,
|
||||||
): SaveWalletUseCase {
|
): SaveWalletUseCase {
|
||||||
return SaveWalletUseCase(
|
return SaveWalletUseCase(
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
walletsRepository = walletsRepository,
|
walletsRepository = walletsRepository,
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -144,15 +98,9 @@ internal object WalletsDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesIsWalletAlreadySavedUseCase(
|
fun providesIsWalletAlreadySavedUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): IsWalletAlreadySavedUseCase {
|
): IsWalletAlreadySavedUseCase {
|
||||||
return IsWalletAlreadySavedUseCase(
|
return IsWalletAlreadySavedUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -167,12 +115,6 @@ internal object WalletsDomainModule {
|
||||||
return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade)
|
return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesUnlockWalletsUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase {
|
|
||||||
return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesUnlockWalletUseCase(
|
fun providesUnlockWalletUseCase(
|
||||||
|
|
@ -200,31 +142,19 @@ internal object WalletsDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesSelectWalletUseCase(
|
fun providesSelectWalletUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
reduxStateHolder: ReduxStateHolder,
|
reduxStateHolder: ReduxStateHolder,
|
||||||
): SelectWalletUseCase {
|
): SelectWalletUseCase {
|
||||||
return SelectWalletUseCase(
|
return SelectWalletUseCase(
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
reduxStateHolder = reduxStateHolder,
|
reduxStateHolder = reduxStateHolder,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesUpdateWalletUseCase(
|
fun providesUpdateWalletUseCase(userWalletsListRepository: UserWalletsListRepository): UpdateWalletUseCase {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return UpdateWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): UpdateWalletUseCase {
|
|
||||||
return UpdateWalletUseCase(
|
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -241,44 +171,14 @@ internal object WalletsDomainModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGetWalletsSyncUseCase(
|
fun providesGetWalletsSyncUseCase(userWalletsListRepository: UserWalletsListRepository): GetWalletNamesUseCase {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return GetWalletNamesUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GetWalletNamesUseCase {
|
|
||||||
return GetWalletNamesUseCase(
|
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesDeleteWalletUseCase(
|
fun providesDeleteWalletUseCase(userWalletsListRepository: UserWalletsListRepository): DeleteWalletUseCase {
|
||||||
userWalletsListManager: UserWalletsListManager,
|
return DeleteWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): DeleteWalletUseCase {
|
|
||||||
return DeleteWalletUseCase(
|
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesShouldSaveUserWalletsSyncUseCase(
|
|
||||||
walletsRepository: WalletsRepository,
|
|
||||||
): ShouldSaveUserWalletsSyncUseCase {
|
|
||||||
return ShouldSaveUserWalletsSyncUseCase(walletsRepository = walletsRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase {
|
|
||||||
return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -350,15 +250,9 @@ internal object WalletsDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGetSavedWalletChangesIdUseCase(
|
fun providesGetSavedWalletChangesIdUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
): GetSavedWalletsCountUseCase {
|
): GetSavedWalletsCountUseCase {
|
||||||
return GetSavedWalletsCountUseCase(
|
return GetSavedWalletsCountUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
|
||||||
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
@ -482,15 +376,11 @@ internal object WalletsDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideGetWalletsForAutomaticallyPushEnablingUseCase(
|
fun provideGetWalletsForAutomaticallyPushEnablingUseCase(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
dispatcherProvider: CoroutineDispatcherProvider,
|
dispatcherProvider: CoroutineDispatcherProvider,
|
||||||
): GetWalletsForAutomaticallyPushEnablingUseCase {
|
): GetWalletsForAutomaticallyPushEnablingUseCase {
|
||||||
return GetWalletsForAutomaticallyPushEnablingUseCase(
|
return GetWalletsForAutomaticallyPushEnablingUseCase(
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
shouldUseNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
dispatchers = dispatcherProvider,
|
dispatchers = dispatcherProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -508,4 +398,12 @@ internal object WalletsDomainModule {
|
||||||
fun provideSyncWalletWithRemoteUseCase(walletsRepository: WalletsRepository): SyncWalletWithRemoteUseCase {
|
fun provideSyncWalletWithRemoteUseCase(walletsRepository: WalletsRepository): SyncWalletWithRemoteUseCase {
|
||||||
return SyncWalletWithRemoteUseCase(walletsRepository = walletsRepository)
|
return SyncWalletWithRemoteUseCase(walletsRepository = walletsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideApplyUserWalletListSortingUseCase(
|
||||||
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
|
): ApplyUserWalletListSortingUseCase {
|
||||||
|
return ApplyUserWalletListSortingUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,19 +16,14 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.hot.sdk.TangemHotSdk
|
import com.tangem.hot.sdk.TangemHotSdk
|
||||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||||
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
import com.tangem.sdk.storage.AndroidSecureStorageV2
|
||||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||||
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
|
|
||||||
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
|
|
||||||
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
|
import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository
|
||||||
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
|
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
|
||||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
|
import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository
|
||||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
|
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
|
||||||
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
|
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
|
||||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
|
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
|
||||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
|
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
|
||||||
|
|
@ -47,77 +42,6 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
internal object UserWalletsListManagerModule {
|
internal object UserWalletsListManagerModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
@Deprecated("Use UserWalletsListRepository instead")
|
|
||||||
fun provideGeneralUserWalletsListManager(
|
|
||||||
@ApplicationContext applicationContext: Context,
|
|
||||||
appPreferencesStore: AppPreferencesStore,
|
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
|
||||||
analyticsEventHandler: AnalyticsEventHandler,
|
|
||||||
): UserWalletsListManager {
|
|
||||||
return GeneralUserWalletsListManager(
|
|
||||||
runtimeUserWalletsListManager = RuntimeUserWalletsListManager(),
|
|
||||||
biometricUserWalletsListManager = createBiometricUserWalletsListManager(
|
|
||||||
applicationContext = applicationContext,
|
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
|
||||||
dispatchers = dispatchers,
|
|
||||||
),
|
|
||||||
appPreferencesStore = appPreferencesStore,
|
|
||||||
dispatchers = dispatchers,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Use UserWalletsListRepository instead")
|
|
||||||
private fun createBiometricUserWalletsListManager(
|
|
||||||
applicationContext: Context,
|
|
||||||
analyticsEventHandler: AnalyticsEventHandler,
|
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
|
||||||
): UserWalletsListManager {
|
|
||||||
val moshi = buildMoshi()
|
|
||||||
val secureStorage = buildSecureStorage(applicationContext = applicationContext)
|
|
||||||
|
|
||||||
val authenticatedStorage = AuthenticatedStorage(
|
|
||||||
secureStorage = UserWalletsKeysStoreDecorator(
|
|
||||||
featureStorage = secureStorage,
|
|
||||||
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
|
|
||||||
),
|
|
||||||
keystoreManager = DelegatedKeystoreManager(
|
|
||||||
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val keysRepository = BiometricUserWalletsKeysRepository(
|
|
||||||
moshi = moshi,
|
|
||||||
secureStorage = secureStorage,
|
|
||||||
authenticatedStorage = authenticatedStorage,
|
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
|
||||||
)
|
|
||||||
|
|
||||||
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
|
|
||||||
moshi = moshi,
|
|
||||||
secureStorage = secureStorage,
|
|
||||||
)
|
|
||||||
|
|
||||||
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
|
|
||||||
moshi = moshi,
|
|
||||||
secureStorage = secureStorage,
|
|
||||||
)
|
|
||||||
|
|
||||||
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
|
|
||||||
secureStorage = secureStorage,
|
|
||||||
dispatchers = dispatchers,
|
|
||||||
)
|
|
||||||
|
|
||||||
return BiometricUserWalletsListManager(
|
|
||||||
keysRepository = keysRepository,
|
|
||||||
publicInformationRepository = publicInformationRepository,
|
|
||||||
sensitiveInformationRepository = sensitiveInformationRepository,
|
|
||||||
selectedUserWalletRepository = selectedUserWalletRepository,
|
|
||||||
dispatcherProvider = dispatchers,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideUserWalletsListRepository(
|
fun provideUserWalletsListRepository(
|
||||||
|
|
@ -183,7 +107,7 @@ internal object UserWalletsListManagerModule {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildMoshi(): Moshi {
|
private fun buildMoshi(): Moshi {
|
||||||
return Moshi.Builder()
|
return Moshi.Builder()
|
||||||
.add(WalletDerivedKeysMapAdapter())
|
.add(WalletDerivedKeysMapAdapter())
|
||||||
.add(ScanResponseDerivedKeysMapAdapter())
|
.add(ScanResponseDerivedKeysMapAdapter())
|
||||||
|
|
@ -200,7 +124,7 @@ internal object UserWalletsListManagerModule {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
|
private fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage {
|
||||||
return AndroidSecureStorage(
|
return AndroidSecureStorage(
|
||||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
|
|
|
||||||
|
|
@ -1,434 +0,0 @@
|
||||||
package com.tangem.tap.domain.userWalletList.implementation
|
|
||||||
|
|
||||||
import com.tangem.common.*
|
|
||||||
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.wallets.legacy.UserWalletsListError
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
|
|
||||||
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
|
||||||
import com.tangem.tap.domain.userWalletList.utils.lockAll
|
|
||||||
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
|
|
||||||
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
@Suppress("LargeClass")
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class BiometricUserWalletsListManager(
|
|
||||||
private val keysRepository: UserWalletsKeysRepository,
|
|
||||||
private val publicInformationRepository: UserWalletsPublicInformationRepository,
|
|
||||||
private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository,
|
|
||||||
private val selectedUserWalletRepository: SelectedUserWalletRepository,
|
|
||||||
private val dispatcherProvider: CoroutineDispatcherProvider,
|
|
||||||
) : UserWalletsListManager.Lockable {
|
|
||||||
private val state = MutableStateFlow(State())
|
|
||||||
|
|
||||||
private var hasSavedWallets: Boolean? = null
|
|
||||||
private val savedWalletMutex = Mutex()
|
|
||||||
|
|
||||||
override val isLockable: Boolean = true
|
|
||||||
|
|
||||||
override val userWallets: Flow<List<UserWallet>>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { it.userWallets }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
override val userWalletsSync: List<UserWallet>
|
|
||||||
get() = state.value.userWallets
|
|
||||||
|
|
||||||
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
|
|
||||||
override val selectedUserWallet: Flow<UserWallet>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { state ->
|
|
||||||
findSelectedUserWallet(state.userWallets)
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
|
|
||||||
override val selectedUserWalletSync: UserWallet?
|
|
||||||
get() = findSelectedUserWallet()
|
|
||||||
|
|
||||||
override val lockedState: Flow<Boolean>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { it.isLocked }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
override val isLocked: Boolean
|
|
||||||
get() = state.value.isLocked
|
|
||||||
|
|
||||||
override val hasUserWallets: Boolean
|
|
||||||
get() {
|
|
||||||
return runBlocking {
|
|
||||||
// workaround to avoid calling hasSavedEncryptionKeys many times because of performance
|
|
||||||
savedWalletMutex.withLock {
|
|
||||||
Timber.i("Checking if user has saved wallets")
|
|
||||||
val hasSavedWalletsLocal = hasSavedWallets
|
|
||||||
if (hasSavedWalletsLocal == null || !hasSavedWalletsLocal) {
|
|
||||||
val hasKeys = keysRepository.hasSavedEncryptionKeys()
|
|
||||||
hasSavedWallets = hasKeys
|
|
||||||
hasKeys
|
|
||||||
} else {
|
|
||||||
Timber.i("User has saved wallets (from cache)")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val walletsCount: Int
|
|
||||||
get() = state.value.userWallets.size
|
|
||||||
|
|
||||||
override val savedWalletsCount: Flow<Int>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { walletsCount }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> {
|
|
||||||
return withContext(dispatcherProvider.io) {
|
|
||||||
unlockAndSetSelectedUserWallet(type)
|
|
||||||
.mapFailure { error ->
|
|
||||||
Timber.e(error, "Unable to unlock user wallets")
|
|
||||||
if (error is UserWalletsListError) {
|
|
||||||
error
|
|
||||||
} else {
|
|
||||||
UserWalletsListError.UnableToUnlockUserWallets(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { selectedUserWallet ->
|
|
||||||
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
|
|
||||||
Timber.e("Unable to find selected user wallet")
|
|
||||||
throw UserWalletsListError.NoUserWalletSelected
|
|
||||||
} else {
|
|
||||||
selectedUserWallet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lock() {
|
|
||||||
state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
encryptionKeys = emptyList(),
|
|
||||||
userWallets = prevState.userWallets.lockAll(),
|
|
||||||
isLocked = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
|
|
||||||
if (state.value.selectedUserWalletId == userWalletId) {
|
|
||||||
return@catching requireNotNull(findSelectedUserWallet()) {
|
|
||||||
"Wallet is not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedUserWalletRepository.set(userWalletId)
|
|
||||||
|
|
||||||
val newState = state.updateAndGet { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
selectedUserWalletId = userWalletId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
newState.userWallets.first { it.walletId == userWalletId }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
|
||||||
return withContext(dispatcherProvider.io) {
|
|
||||||
if (canOverride) {
|
|
||||||
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
|
|
||||||
} else {
|
|
||||||
val isWalletSaved = state.value.userWallets
|
|
||||||
.any {
|
|
||||||
it.walletId == userWallet.walletId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWalletSaved) {
|
|
||||||
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
|
|
||||||
} else {
|
|
||||||
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(
|
|
||||||
userWalletId: UserWalletId,
|
|
||||||
update: suspend (UserWallet) -> UserWallet,
|
|
||||||
): CompletionResult<UserWallet> {
|
|
||||||
return withContext(dispatcherProvider.io) {
|
|
||||||
get(userWalletId)
|
|
||||||
.map { storedUserWallet ->
|
|
||||||
update(storedUserWallet)
|
|
||||||
}
|
|
||||||
.flatMap { updatedUserWallet ->
|
|
||||||
saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true)
|
|
||||||
}
|
|
||||||
.flatMap {
|
|
||||||
get(userWalletId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
|
|
||||||
val idsToRemove = state.value.userWallets
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.filter { it.walletId in userWalletIds }
|
|
||||||
?.map { it.walletId }
|
|
||||||
|
|
||||||
if (idsToRemove.isNullOrEmpty()) {
|
|
||||||
return CompletionResult.Success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idsToRemove.size == state.value.userWallets.size) {
|
|
||||||
return clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
return withContext(dispatcherProvider.io) {
|
|
||||||
sensitiveInformationRepository.delete(idsToRemove)
|
|
||||||
.flatMap { publicInformationRepository.delete(idsToRemove) }
|
|
||||||
.map { keysRepository.delete(idsToRemove) }
|
|
||||||
.map {
|
|
||||||
state.update { prevState ->
|
|
||||||
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
|
|
||||||
|
|
||||||
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
|
|
||||||
val newSelectedUserWallet = findOrSetSelectedWallet(
|
|
||||||
prevSelectedWalletId = prevState.selectedUserWalletId,
|
|
||||||
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
|
|
||||||
it.walletId == prevState.selectedUserWalletId
|
|
||||||
},
|
|
||||||
userWallets = remainingWallets,
|
|
||||||
ignorePrevSelectedWallet = isSelectedWalletDeleted,
|
|
||||||
)
|
|
||||||
|
|
||||||
prevState.copy(
|
|
||||||
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
|
|
||||||
userWallets = remainingWallets,
|
|
||||||
isLocked = remainingWallets.any { it.isLocked },
|
|
||||||
selectedUserWalletId = newSelectedUserWallet?.walletId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clear(): CompletionResult<Unit> {
|
|
||||||
savedWalletMutex.withLock {
|
|
||||||
hasSavedWallets = null
|
|
||||||
}
|
|
||||||
return withContext(dispatcherProvider.io) {
|
|
||||||
sensitiveInformationRepository.clear()
|
|
||||||
.flatMap { publicInformationRepository.clear() }
|
|
||||||
.map {
|
|
||||||
keysRepository.clear()
|
|
||||||
selectedUserWalletRepository.set(null)
|
|
||||||
state.value = State()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
|
||||||
return catching {
|
|
||||||
state.value.userWallets.first { it.walletId == userWalletId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveInternal(
|
|
||||||
userWallet: UserWallet,
|
|
||||||
changeSelectedUserWallet: Boolean,
|
|
||||||
canOverridePublicInfo: Boolean,
|
|
||||||
): CompletionResult<Unit> {
|
|
||||||
val encryptionKey = userWallet.encryptionKey
|
|
||||||
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
|
|
||||||
?: return CompletionResult.Success(Unit) // No encryption key, no need to save
|
|
||||||
|
|
||||||
return keysRepository.save(encryptionKey)
|
|
||||||
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = encryptionKey.encryptionKey) }
|
|
||||||
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
|
|
||||||
.flatMap {
|
|
||||||
loadUserWallets(
|
|
||||||
encryptionKeys = state.value.encryptionKeys
|
|
||||||
.plus(encryptionKey)
|
|
||||||
.distinctBy(UserWalletEncryptionKey::walletId),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.doOnSuccess { loadedState ->
|
|
||||||
if (changeSelectedUserWallet) {
|
|
||||||
selectedUserWalletRepository.set(userWallet.walletId)
|
|
||||||
|
|
||||||
state.value = loadedState.copy(
|
|
||||||
selectedUserWalletId = userWallet.walletId,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
state.value = loadedState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { /* Type erasing */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun unlockAndSetSelectedUserWallet(type: UnlockType): CompletionResult<UserWallet?> {
|
|
||||||
return keysRepository.getAll()
|
|
||||||
.flatMap { encryptionKeys ->
|
|
||||||
loadUserWallets(
|
|
||||||
encryptionKeys = state.value.encryptionKeys
|
|
||||||
.plus(encryptionKeys)
|
|
||||||
.distinctBy(UserWalletEncryptionKey::walletId),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map { loadedState ->
|
|
||||||
when (type) {
|
|
||||||
UnlockType.ALL -> {
|
|
||||||
if (loadedState.isLocked) {
|
|
||||||
Timber.e("Some user wallets remain locked")
|
|
||||||
|
|
||||||
state.value = loadedState
|
|
||||||
|
|
||||||
throw UserWalletsListError.NotAllUserWalletsUnlocked
|
|
||||||
} else {
|
|
||||||
val prevState = state.value
|
|
||||||
|
|
||||||
val selectedWallet = findOrSetSelectedWallet(
|
|
||||||
prevSelectedWalletId = prevState.selectedUserWalletId,
|
|
||||||
userWallets = loadedState.userWallets,
|
|
||||||
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
|
|
||||||
it.walletId == prevState.selectedUserWalletId
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId)
|
|
||||||
|
|
||||||
selectedWallet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UnlockType.ANY -> {
|
|
||||||
val prevState = state.value
|
|
||||||
|
|
||||||
val selectedWallet = findOrSetSelectedWallet(
|
|
||||||
prevSelectedWalletId = state.value.selectedUserWalletId,
|
|
||||||
prevSelectedWalletIndex = prevState.userWallets.indexOfFirst {
|
|
||||||
it.walletId == prevState.selectedUserWalletId
|
|
||||||
},
|
|
||||||
userWallets = loadedState.userWallets,
|
|
||||||
)
|
|
||||||
|
|
||||||
state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId)
|
|
||||||
|
|
||||||
selectedWallet
|
|
||||||
}
|
|
||||||
UnlockType.ALL_WITHOUT_SELECT -> {
|
|
||||||
state.value = loadedState
|
|
||||||
|
|
||||||
findSelectedUserWallet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadUserWallets(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<State> {
|
|
||||||
return publicInformationRepository.getAll()
|
|
||||||
.map { it.toUserWallets() }
|
|
||||||
.flatMap { userWallets ->
|
|
||||||
sensitiveInformationRepository.getAll(encryptionKeys)
|
|
||||||
.map { walletIdToSensitiveInformation ->
|
|
||||||
userWallets.updateWith(walletIdToSensitiveInformation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { userWallets ->
|
|
||||||
val prevState = state.value
|
|
||||||
|
|
||||||
if (userWallets.isNotEmpty()) {
|
|
||||||
val newUserWallets = (userWallets + prevState.userWallets)
|
|
||||||
.distinctBy(UserWallet::walletId)
|
|
||||||
|
|
||||||
prevState.copy(
|
|
||||||
userWallets = newUserWallets,
|
|
||||||
encryptionKeys = encryptionKeys,
|
|
||||||
isLocked = newUserWallets.any(UserWallet::isLocked),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
prevState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun findOrSetSelectedWallet(
|
|
||||||
prevSelectedWalletId: UserWalletId?,
|
|
||||||
prevSelectedWalletIndex: Int,
|
|
||||||
userWallets: List<UserWallet>,
|
|
||||||
ignorePrevSelectedWallet: Boolean = false,
|
|
||||||
): UserWallet? {
|
|
||||||
var possibleSelectedUserWallet: UserWallet? = null
|
|
||||||
|
|
||||||
if (!ignorePrevSelectedWallet) {
|
|
||||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
|
||||||
possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
|
|
||||||
possibleSelectedUserWallet =
|
|
||||||
userWallets.findAvailableUserWallet(prevSelectedIndex = prevSelectedWalletIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
|
|
||||||
|
|
||||||
return possibleSelectedUserWallet
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the nearest available wallet that can be selected
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* Number with *n* is previous selected wallet with index [prevSelectedIndex].
|
|
||||||
*
|
|
||||||
* 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4]
|
|
||||||
* 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4]
|
|
||||||
* 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*]
|
|
||||||
* 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*]
|
|
||||||
*
|
|
||||||
* @receiver list of user wallets without deleted wallet
|
|
||||||
*/
|
|
||||||
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
|
|
||||||
if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull()
|
|
||||||
|
|
||||||
if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex]
|
|
||||||
|
|
||||||
for (offset in 1..size) {
|
|
||||||
val rightIndex = prevSelectedIndex + offset
|
|
||||||
if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex]
|
|
||||||
|
|
||||||
val leftIndex = prevSelectedIndex - offset
|
|
||||||
if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
return lastOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findSelectedUserWallet(
|
|
||||||
userWallets: List<UserWallet> = state.value.userWallets,
|
|
||||||
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,
|
|
||||||
): UserWallet? {
|
|
||||||
return userWallets.firstOrNull { it.walletId == selectedUserWalletId }
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class State(
|
|
||||||
val encryptionKeys: List<UserWalletEncryptionKey> = emptyList(),
|
|
||||||
val userWallets: List<UserWallet> = emptyList(),
|
|
||||||
val selectedUserWalletId: UserWalletId? = null,
|
|
||||||
val isLocked: Boolean = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,203 +0,0 @@
|
||||||
package com.tangem.tap.domain.userWalletList.implementation
|
|
||||||
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
|
||||||
import com.tangem.datasource.local.preferences.utils.get
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General implementation of [UserWalletsListManager] that helps to switch between Runtime and Biometric
|
|
||||||
* implementations.
|
|
||||||
*
|
|
||||||
* @property runtimeUserWalletsListManager runtime user wallets list manager
|
|
||||||
* @property biometricUserWalletsListManager biometric user wallets list manager
|
|
||||||
* @property appPreferencesStore app preferences store
|
|
||||||
* @property dispatchers coroutine dispatcher provider
|
|
||||||
*
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class GeneralUserWalletsListManager(
|
|
||||||
private val runtimeUserWalletsListManager: UserWalletsListManager,
|
|
||||||
private val biometricUserWalletsListManager: UserWalletsListManager,
|
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
|
||||||
) : UserWalletsListManager.Lockable {
|
|
||||||
|
|
||||||
private val applicationScope = CoroutineScope(dispatchers.io)
|
|
||||||
private val implementation: MutableStateFlow<UserWalletsListManager?> = MutableStateFlow(value = null)
|
|
||||||
|
|
||||||
private val requireImplementation: UserWalletsListManager
|
|
||||||
get() = requireNotNull(implementation.value) {
|
|
||||||
"UserWalletsListManager is not initialized"
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
subscribeOnCurrentManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isLockable: Boolean
|
|
||||||
get() = requireImplementation.isLockable
|
|
||||||
|
|
||||||
override val userWallets: Flow<List<UserWallet>>
|
|
||||||
get() = implementation
|
|
||||||
.transformLatest { impl ->
|
|
||||||
if (impl != null) {
|
|
||||||
emitAll(impl.userWallets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// To avoid returning empty flow to subscriber while implementation and userWallets are null
|
|
||||||
// Flow is called first time when implementation is null and then when its assigned with implementation
|
|
||||||
// that may have not user wallets (null or empty).
|
|
||||||
// As a result subscription occurs on empty flow, than will not change if user wallets are available
|
|
||||||
.filter { requireImplementation.hasUserWallets }
|
|
||||||
|
|
||||||
override val savedWalletsCount: Flow<Int>
|
|
||||||
get() = implementation
|
|
||||||
.transformLatest { impl ->
|
|
||||||
if (impl != null) {
|
|
||||||
emitAll(impl.savedWalletsCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val userWalletsSync: List<UserWallet>
|
|
||||||
get() = requireImplementation.userWalletsSync
|
|
||||||
|
|
||||||
override val selectedUserWallet: Flow<UserWallet>
|
|
||||||
get() = implementation
|
|
||||||
.transformLatest { impl ->
|
|
||||||
if (impl != null) {
|
|
||||||
emitAll(impl.selectedUserWallet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// To avoid returning empty flow to subscriber while implementation and userWallets are null
|
|
||||||
// Flow is called first time when implementation is null and then when its assigned with implementation
|
|
||||||
// that may have not user wallets (null or empty).
|
|
||||||
// As a result subscription occurs on empty flow, than will not change if user wallets are available
|
|
||||||
.filter { requireImplementation.hasUserWallets }
|
|
||||||
|
|
||||||
override val selectedUserWalletSync: UserWallet?
|
|
||||||
get() = requireImplementation.selectedUserWalletSync
|
|
||||||
|
|
||||||
override val hasUserWallets: Boolean
|
|
||||||
get() = requireImplementation.hasUserWallets
|
|
||||||
|
|
||||||
override val walletsCount: Int
|
|
||||||
get() = requireImplementation.walletsCount
|
|
||||||
|
|
||||||
override val lockedState: Flow<Boolean>
|
|
||||||
get() = implementation.transformLatest { impl ->
|
|
||||||
if (impl == null) return@transformLatest
|
|
||||||
|
|
||||||
if (impl is UserWalletsListManager.Lockable) {
|
|
||||||
emitAll(impl.lockedState)
|
|
||||||
} else {
|
|
||||||
error("RuntimeUserWalletsListManager is not lockable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isLocked: Boolean
|
|
||||||
get() {
|
|
||||||
val impl = requireImplementation
|
|
||||||
|
|
||||||
return if (impl is UserWalletsListManager.Lockable) {
|
|
||||||
impl.isLocked
|
|
||||||
} else {
|
|
||||||
error("RuntimeUserWalletsListManager is not lockable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
|
||||||
return requireImplementation.select(userWalletId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
|
||||||
return requireImplementation.save(userWallet, canOverride)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(
|
|
||||||
userWalletId: UserWalletId,
|
|
||||||
update: suspend (UserWallet) -> UserWallet,
|
|
||||||
): CompletionResult<UserWallet> {
|
|
||||||
return requireImplementation.update(userWalletId, update)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
|
|
||||||
return requireImplementation.delete(userWalletIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clear(): CompletionResult<Unit> {
|
|
||||||
return requireImplementation.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
|
|
||||||
return requireImplementation.get(userWalletId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult<UserWallet> {
|
|
||||||
val implementation = requireImplementation
|
|
||||||
|
|
||||||
return if (implementation is UserWalletsListManager.Lockable) {
|
|
||||||
implementation.unlock(type)
|
|
||||||
} else {
|
|
||||||
error("RuntimeUserWalletsListManager is not lockable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lock() {
|
|
||||||
val implementation = requireImplementation
|
|
||||||
|
|
||||||
return if (implementation is UserWalletsListManager.Lockable) {
|
|
||||||
implementation.lock()
|
|
||||||
} else {
|
|
||||||
error("RuntimeUserWalletsListManager is not lockable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun subscribeOnCurrentManager() {
|
|
||||||
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.onEach { shouldSaveUserWallets ->
|
|
||||||
val possibleManager = if (shouldSaveUserWallets) {
|
|
||||||
biometricUserWalletsListManager
|
|
||||||
} else {
|
|
||||||
runtimeUserWalletsListManager
|
|
||||||
}
|
|
||||||
|
|
||||||
if (possibleManager == implementation.value) {
|
|
||||||
Timber.e("Switch to the same manager ${possibleManager::class.simpleName.orEmpty()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Switch to ${possibleManager::class.simpleName.orEmpty()}")
|
|
||||||
|
|
||||||
val previousManager = implementation.value
|
|
||||||
implementation.value = copySelectedUserWallet(
|
|
||||||
sourceManager = previousManager,
|
|
||||||
destinationManager = possibleManager,
|
|
||||||
)
|
|
||||||
|
|
||||||
previousManager?.clear()
|
|
||||||
}
|
|
||||||
.flowOn(dispatchers.io)
|
|
||||||
.launchIn(applicationScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun copySelectedUserWallet(
|
|
||||||
sourceManager: UserWalletsListManager?,
|
|
||||||
destinationManager: UserWalletsListManager,
|
|
||||||
): UserWalletsListManager {
|
|
||||||
sourceManager?.selectedUserWalletSync?.let { selectedWallet ->
|
|
||||||
destinationManager.save(selectedWallet, canOverride = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinationManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
package com.tangem.tap.domain.userWalletList.implementation
|
|
||||||
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.common.catching
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
|
|
||||||
private val state = MutableStateFlow(State())
|
|
||||||
|
|
||||||
override val isLockable: Boolean = false
|
|
||||||
|
|
||||||
override val userWallets: Flow<List<UserWallet>>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { listOfNotNull(it.userWallet) }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
override val selectedUserWallet: Flow<UserWallet>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { it.userWallet }
|
|
||||||
.filterNotNull()
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
override val userWalletsSync: List<UserWallet>
|
|
||||||
get() = listOfNotNull(state.value.userWallet)
|
|
||||||
|
|
||||||
override val selectedUserWalletSync: UserWallet?
|
|
||||||
get() = state.value.userWallet
|
|
||||||
|
|
||||||
override val hasUserWallets: Boolean
|
|
||||||
get() = state.value.userWallet != null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* only 1 wallet stored in runtime implementation
|
|
||||||
*/
|
|
||||||
override val walletsCount: Int
|
|
||||||
get() = if (hasUserWallets) 1 else 0
|
|
||||||
|
|
||||||
override val savedWalletsCount: Flow<Int>
|
|
||||||
get() = state
|
|
||||||
.mapLatest { walletsCount }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
|
|
||||||
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
|
|
||||||
state.value.userWallet
|
|
||||||
?.takeIf { it.walletId == userWalletId }
|
|
||||||
?: walletNotFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
|
|
||||||
return if (canOverride) {
|
|
||||||
saveInternal(userWallet)
|
|
||||||
} else {
|
|
||||||
val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId
|
|
||||||
|
|
||||||
if (isWalletSaved) {
|
|
||||||
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
|
|
||||||
} else {
|
|
||||||
saveInternal(userWallet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(
|
|
||||||
userWalletId: UserWalletId,
|
|
||||||
update: suspend (UserWallet) -> UserWallet,
|
|
||||||
): CompletionResult<UserWallet> = catching {
|
|
||||||
val wallet = state.value.userWallet
|
|
||||||
?.takeIf { it.walletId == userWalletId }
|
|
||||||
?: walletNotFound()
|
|
||||||
|
|
||||||
requireNotNull(
|
|
||||||
state.updateAndGet { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
userWallet = update(wallet),
|
|
||||||
)
|
|
||||||
}.userWallet,
|
|
||||||
) { "User wallet is null after update" }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> = clear()
|
|
||||||
|
|
||||||
override suspend fun clear(): CompletionResult<Unit> = catching {
|
|
||||||
state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
userWallet = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
|
|
||||||
state.value.userWallet ?: walletNotFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
|
|
||||||
state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
userWallet = userWallet,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun walletNotFound(): Nothing {
|
|
||||||
throw NoSuchElementException("User wallet not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class State(
|
|
||||||
val userWallet: UserWallet? = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -13,6 +13,7 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||||
|
import com.tangem.domain.common.wallets.UserWalletTransformAction
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
|
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
|
||||||
import com.tangem.domain.common.wallets.error.*
|
import com.tangem.domain.common.wallets.error.*
|
||||||
|
|
@ -31,6 +32,8 @@ import com.tangem.sdk.api.TangemSdkManager
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||||
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
|
||||||
import com.tangem.tap.domain.userWalletList.utils.lock
|
import com.tangem.tap.domain.userWalletList.utils.lock
|
||||||
|
import com.tangem.tap.domain.userWalletList.utils.publicInformation
|
||||||
|
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
|
||||||
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
|
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
|
||||||
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
import com.tangem.tap.domain.userWalletList.utils.updateWith
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
|
|
@ -38,10 +41,12 @@ import com.tangem.utils.ProviderSuspend
|
||||||
import com.tangem.utils.coroutines.runSuspendCatching
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
import com.tangem.utils.extensions.addOrReplace
|
import com.tangem.utils.extensions.addOrReplace
|
||||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Suppress("LongParameterList", "LargeClass")
|
@Suppress("LongParameterList", "LargeClass")
|
||||||
internal class DefaultUserWalletsListRepository(
|
internal class DefaultUserWalletsListRepository(
|
||||||
|
|
@ -384,6 +389,44 @@ internal class DefaultUserWalletsListRepository(
|
||||||
return userWallets.any { it.walletId !in unsecuredWalletIds }
|
return userWallets.any { it.walletId !in unsecuredWalletIds }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun transform(action: UserWalletTransformAction) {
|
||||||
|
val wallets = userWalletsSync()
|
||||||
|
val walletsMap = wallets.associateBy { it.walletId }
|
||||||
|
val transformedWallets = action.transform(wallets)
|
||||||
|
val transformedWalletsMap = transformedWallets.associateBy { it.walletId }
|
||||||
|
|
||||||
|
require(walletsMap.keys == transformedWalletsMap.keys) {
|
||||||
|
"The transformation action must not change the set of wallet IDs." +
|
||||||
|
"Original IDs: ${walletsMap.keys}, Transformed IDs: ${transformedWalletsMap.keys}"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWallets { transformedWallets }
|
||||||
|
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
if (savePersistentInformation()) {
|
||||||
|
publicInformationRepository.transform {
|
||||||
|
transformedWallets.map { wallet -> wallet.publicInformation }
|
||||||
|
}
|
||||||
|
|
||||||
|
val changedSensitiveInfoWallets = wallets.filter { wallet ->
|
||||||
|
val transformedWallet = transformedWalletsMap[wallet.walletId]
|
||||||
|
transformedWallet != null && transformedWallet.sensitiveInformation != wallet.sensitiveInformation
|
||||||
|
}
|
||||||
|
|
||||||
|
changedSensitiveInfoWallets.forEach { wallet ->
|
||||||
|
if (wallet.isLocked.not()) {
|
||||||
|
sensitiveInformationRepository.save(wallet, wallet.encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForUpgradeAndDeleteHotWalletIfNeeded(
|
||||||
|
newUserWallet = wallet,
|
||||||
|
oldUserWallet = walletsMap[wallet.walletId] ?: error("This should never happen"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun checkForUpgradeAndDeleteHotWalletIfNeeded(
|
private suspend fun checkForUpgradeAndDeleteHotWalletIfNeeded(
|
||||||
newUserWallet: UserWallet,
|
newUserWallet: UserWallet,
|
||||||
oldUserWallet: UserWallet,
|
oldUserWallet: UserWallet,
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,8 @@ internal interface UserWalletsPublicInformationRepository {
|
||||||
|
|
||||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||||
suspend fun clear(): CompletionResult<Unit>
|
suspend fun clear(): CompletionResult<Unit>
|
||||||
|
|
||||||
|
suspend fun transform(
|
||||||
|
block: (List<UserWalletPublicInformation>?) -> List<UserWalletPublicInformation>?,
|
||||||
|
): CompletionResult<Unit>
|
||||||
}
|
}
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
|
||||||
|
|
||||||
import com.tangem.common.core.TangemError
|
|
||||||
import com.tangem.common.core.TangemSdkError
|
|
||||||
import com.tangem.core.analytics.models.Basic
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
|
||||||
import com.tangem.utils.converter.Converter
|
|
||||||
|
|
||||||
object BiometricFailReasonConverter : Converter<TangemError, Basic.BiometryFailed.BiometricFailReason> {
|
|
||||||
|
|
||||||
override fun convert(value: TangemError): Basic.BiometryFailed.BiometricFailReason {
|
|
||||||
// 1. Try handle TangemSdkError cases first if error has not been mapped
|
|
||||||
when (value) {
|
|
||||||
is TangemSdkError.AuthenticationCanceled ->
|
|
||||||
return Basic.BiometryFailed.BiometricFailReason.AuthenticationCancelled
|
|
||||||
is TangemSdkError.AuthenticationAlreadyInProgress ->
|
|
||||||
return Basic.BiometryFailed.BiometricFailReason.AuthenticationAlreadyInProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. For other errors, check if they are of type UserWalletsListError
|
|
||||||
if (value !is UserWalletsListError) {
|
|
||||||
return Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage)
|
|
||||||
}
|
|
||||||
// 3. Map UserWalletsListError to BiometricFailReason
|
|
||||||
return when (value) {
|
|
||||||
UserWalletsListError.AllKeysInvalidated ->
|
|
||||||
Basic.BiometryFailed.BiometricFailReason.AllKeysInvalidated
|
|
||||||
UserWalletsListError.BiometricsAuthenticationDisabled ->
|
|
||||||
Basic.BiometryFailed.BiometricFailReason.BiometricsAuthenticationDisabled
|
|
||||||
is UserWalletsListError.BiometricsAuthenticationLockout ->
|
|
||||||
if (value.isPermanent) {
|
|
||||||
Basic.BiometryFailed.BiometricFailReason.AuthenticationLockoutPermanent
|
|
||||||
} else {
|
|
||||||
Basic.BiometryFailed.BiometricFailReason.AuthenticationLockout
|
|
||||||
}
|
|
||||||
else -> Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
package com.tangem.tap.domain.userWalletList.repository.implementation
|
|
||||||
|
|
||||||
import com.squareup.moshi.JsonAdapter
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.Types
|
|
||||||
import com.tangem.common.*
|
|
||||||
import com.tangem.common.authentication.storage.AuthenticatedStorage
|
|
||||||
import com.tangem.common.core.TangemSdkError
|
|
||||||
import com.tangem.common.services.secure.SecureStorage
|
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
|
||||||
import com.tangem.core.analytics.models.Basic
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
|
||||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
|
||||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
internal class BiometricUserWalletsKeysRepository(
|
|
||||||
moshi: Moshi,
|
|
||||||
private val authenticatedStorage: AuthenticatedStorage,
|
|
||||||
private val secureStorage: SecureStorage,
|
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
|
||||||
) : UserWalletsKeysRepository {
|
|
||||||
|
|
||||||
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
|
|
||||||
UserWalletEncryptionKey::class.java,
|
|
||||||
)
|
|
||||||
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
|
|
||||||
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
getAllInternal()
|
|
||||||
.mapFailure { error ->
|
|
||||||
val mappedError = when (error) {
|
|
||||||
is TangemSdkError.AuthenticationLockout ->
|
|
||||||
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false)
|
|
||||||
is TangemSdkError.AuthenticationPermanentLockout ->
|
|
||||||
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
|
|
||||||
is TangemSdkError.KeystoreInvalidated ->
|
|
||||||
UserWalletsListError.AllKeysInvalidated
|
|
||||||
is TangemSdkError.AuthenticationUnavailable ->
|
|
||||||
UserWalletsListError.BiometricsAuthenticationDisabled
|
|
||||||
else -> error
|
|
||||||
}
|
|
||||||
analyticsEventHandler.send(
|
|
||||||
Basic.BiometryFailed(
|
|
||||||
source = AnalyticsParam.ScreensSources.SignIn,
|
|
||||||
reason = BiometricFailReasonConverter.convert(mappedError),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
mappedError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
storeEncryptionKey(encryptionKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(userWalletsIds: List<UserWalletId>) {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
userWalletsIds.forEach { userWalletId ->
|
|
||||||
deleteEncryptionKey(userWalletId)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteUserWalletsIds(userWalletsIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clear() {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
getUserWalletsIds()
|
|
||||||
.forEach { userWalletId ->
|
|
||||||
deleteEncryptionKey(userWalletId)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearUserWalletsIds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasSavedEncryptionKeys(): Boolean {
|
|
||||||
return runBlocking {
|
|
||||||
getUserWalletsIds().isNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getAllInternal(): CompletionResult<List<UserWalletEncryptionKey>> {
|
|
||||||
return catching {
|
|
||||||
val userWalletIds = getUserWalletsIds()
|
|
||||||
|
|
||||||
getEncryptionKeys(userWalletIds)
|
|
||||||
}
|
|
||||||
.doOnFailure { error ->
|
|
||||||
when (error) {
|
|
||||||
is TangemSdkError.KeystoreInvalidated -> {
|
|
||||||
getUserWalletsIds().forEach { userWalletId ->
|
|
||||||
deleteEncryptionKey(userWalletId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getEncryptionKeys(userWalletsIds: List<UserWalletId>): List<UserWalletEncryptionKey> {
|
|
||||||
val keys = userWalletsIds.map { userWalletId ->
|
|
||||||
StorageKey.UserWalletEncryptionKey(userWalletId).name
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticatedStorage.get(keys).mapNotNull { (_, encodedData) ->
|
|
||||||
encodedData.decodeToKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
|
|
||||||
return catching {
|
|
||||||
authenticatedStorage.store(
|
|
||||||
keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name,
|
|
||||||
data = encryptionKey.encode(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.map { storeUserWalletId(encryptionKey.walletId) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteEncryptionKey(userWalletId: UserWalletId) {
|
|
||||||
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getUserWalletsIds(): List<UserWalletId> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
secureStorage.get(StorageKey.UserWalletIds.name)
|
|
||||||
.decodeToUserWalletsIds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun storeUserWalletId(userWalletId: UserWalletId) {
|
|
||||||
val userWalletIds = (getUserWalletsIds() + userWalletId).distinct()
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteUserWalletsIds(userWalletsIds: List<UserWalletId>) {
|
|
||||||
val remainingIds = getUserWalletsIds() - userWalletsIds.toSet()
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
secureStorage.store(remainingIds.encode(), StorageKey.UserWalletIds.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun clearUserWalletsIds() {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
secureStorage.delete(StorageKey.UserWalletIds.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun UserWalletEncryptionKey.encode(): ByteArray {
|
|
||||||
return withContext(Dispatchers.Default) {
|
|
||||||
encryptionKeyAdapter.toJson(this@encode)
|
|
||||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? {
|
|
||||||
return withContext(Dispatchers.Default) {
|
|
||||||
this@decodeToKey
|
|
||||||
?.decodeToString(throwOnInvalidSequence = true)
|
|
||||||
?.let(encryptionKeyAdapter::fromJson)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun List<UserWalletId>.encode(): ByteArray {
|
|
||||||
return withContext(Dispatchers.Default) {
|
|
||||||
userWalletsIdsListAdapter.toJson(this@encode)
|
|
||||||
.encodeToByteArray(throwOnInvalidSequence = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
|
|
||||||
return withContext(Dispatchers.Default) {
|
|
||||||
this@decodeToUserWalletsIds
|
|
||||||
?.decodeToString(throwOnInvalidSequence = true)
|
|
||||||
?.let(userWalletsIdsListAdapter::fromJson)
|
|
||||||
.orEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed interface StorageKey {
|
|
||||||
val name: String
|
|
||||||
|
|
||||||
class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey {
|
|
||||||
override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}"
|
|
||||||
}
|
|
||||||
|
|
||||||
object UserWalletIds : StorageKey {
|
|
||||||
override val name: String = "user_wallets_ids_with_saved_keys"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -77,6 +77,21 @@ internal class DefaultUserWalletsPublicInformationRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun transform(
|
||||||
|
block: (List<UserWalletPublicInformation>?) -> List<UserWalletPublicInformation>?,
|
||||||
|
): CompletionResult<Unit> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
getAll().flatMap { currentInfo ->
|
||||||
|
val transformed = block(currentInfo)
|
||||||
|
if (transformed != null) {
|
||||||
|
save(transformed)
|
||||||
|
} else {
|
||||||
|
CompletionResult.Success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmName("saveWithPublicInformation")
|
@JvmName("saveWithPublicInformation")
|
||||||
private suspend fun save(publicInformation: List<UserWalletPublicInformation>): CompletionResult<Unit> = catching {
|
private suspend fun save(publicInformation: List<UserWalletPublicInformation>): CompletionResult<Unit> = catching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,6 @@ internal fun List<UserWallet>.updateWith(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun List<UserWallet>.lockAll(): List<UserWallet> = map(UserWallet::lock)
|
|
||||||
|
|
||||||
internal fun UserWallet.lock(): UserWallet = when (this) {
|
internal fun UserWallet.lock(): UserWallet = when (this) {
|
||||||
is UserWallet.Cold -> {
|
is UserWallet.Cold -> {
|
||||||
copy(
|
copy(
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,12 @@ package com.tangem.tap.features.details.redux
|
||||||
import com.tangem.common.CompletionResult
|
import com.tangem.common.CompletionResult
|
||||||
import com.tangem.common.doOnFailure
|
import com.tangem.common.doOnFailure
|
||||||
import com.tangem.common.doOnSuccess
|
import com.tangem.common.doOnSuccess
|
||||||
import com.tangem.common.routing.AppRoute
|
|
||||||
import com.tangem.core.analytics.Analytics
|
import com.tangem.core.analytics.Analytics
|
||||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
|
import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||||
import com.tangem.tap.common.analytics.events.Settings
|
import com.tangem.tap.common.analytics.events.Settings
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
|
||||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||||
import com.tangem.tap.common.extensions.inject
|
import com.tangem.tap.common.extensions.inject
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
|
|
@ -43,7 +40,7 @@ class DetailsMiddleware {
|
||||||
if (!DemoHelper.tryHandle(stateProvider)) {
|
if (!DemoHelper.tryHandle(stateProvider)) {
|
||||||
val detailsState = stateProvider()?.detailsState
|
val detailsState = stateProvider()?.detailsState
|
||||||
if (detailsState != null) {
|
if (detailsState != null) {
|
||||||
handleAction(detailsState, action)
|
handleAction(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next(action)
|
next(action)
|
||||||
|
|
@ -51,9 +48,9 @@ class DetailsMiddleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAction(state: DetailsState, action: Action) {
|
private fun handleAction(action: Action) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
|
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,12 +58,10 @@ class DetailsMiddleware {
|
||||||
|
|
||||||
private val checkBiometricsStatusJobHolder = JobHolder()
|
private val checkBiometricsStatusJobHolder = JobHolder()
|
||||||
|
|
||||||
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
|
fun handle(action: DetailsAction.AppSettings) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||||
when (action.setting) {
|
when (action.setting) {
|
||||||
AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
|
|
||||||
AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
|
|
||||||
AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable)
|
AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable)
|
||||||
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable)
|
AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable)
|
||||||
}
|
}
|
||||||
|
|
@ -221,118 +216,6 @@ class DetailsMiddleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
|
||||||
// Nothing to change
|
|
||||||
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
|
|
||||||
|
|
||||||
if (walletsRepository.shouldSaveUserWalletsSync() == enable) {
|
|
||||||
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSaveWallets(state.scanResponse, enable)
|
|
||||||
.doOnFailure {
|
|
||||||
store.dispatchWithMain(
|
|
||||||
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
|
|
||||||
prevState = !enable,
|
|
||||||
setting = AppSetting.SaveWallets,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.doOnSuccess {
|
|
||||||
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun toggleSaveWallets(scanResponse: ScanResponse?, enable: Boolean): CompletionResult<Unit> {
|
|
||||||
return if (enable) {
|
|
||||||
saveCurrentWallet(scanResponse, enableAccessCodesSaving = false)
|
|
||||||
} else {
|
|
||||||
deleteSavedWalletsAndAccessCodes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch {
|
|
||||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
|
||||||
|
|
||||||
// Nothing to change
|
|
||||||
if (shouldSaveAccessCodes == enable) {
|
|
||||||
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSaveAccessCodes(state.scanResponse, state.appSettingsState.saveWallets, enable)
|
|
||||||
.doOnFailure {
|
|
||||||
store.dispatchWithMain(
|
|
||||||
DetailsAction.AppSettings.SwitchPrivacySetting.Failure(
|
|
||||||
prevState = !enable,
|
|
||||||
setting = AppSetting.SaveAccessCode,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.doOnSuccess {
|
|
||||||
store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun toggleSaveAccessCodes(
|
|
||||||
scanResponse: ScanResponse?,
|
|
||||||
isWalletsSavingEnabled: Boolean,
|
|
||||||
enable: Boolean,
|
|
||||||
): CompletionResult<Unit> {
|
|
||||||
return if (enable) {
|
|
||||||
if (!isWalletsSavingEnabled) {
|
|
||||||
saveCurrentWallet(scanResponse, enableAccessCodesSaving = true)
|
|
||||||
} else {
|
|
||||||
saveAccessCodes(scanResponse)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
deleteSavedAccessCodes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveCurrentWallet(
|
|
||||||
scanResponse: ScanResponse?,
|
|
||||||
enableAccessCodesSaving: Boolean,
|
|
||||||
): CompletionResult<Unit> {
|
|
||||||
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
|
|
||||||
|
|
||||||
return if (enableAccessCodesSaving) {
|
|
||||||
saveAccessCodes(scanResponse)
|
|
||||||
} else {
|
|
||||||
CompletionResult.Success(Unit)
|
|
||||||
}
|
|
||||||
.doOnSuccess {
|
|
||||||
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On))
|
|
||||||
}
|
|
||||||
.doOnFailure { error ->
|
|
||||||
Timber.e(error, "Unable to save user wallet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult<Unit> {
|
|
||||||
Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off))
|
|
||||||
|
|
||||||
deleteSavedAccessCodes()
|
|
||||||
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
|
|
||||||
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
|
|
||||||
|
|
||||||
return CompletionResult.Success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult<Unit> {
|
|
||||||
Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On))
|
|
||||||
|
|
||||||
store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true)
|
|
||||||
|
|
||||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
|
||||||
isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true,
|
|
||||||
)
|
|
||||||
|
|
||||||
return CompletionResult.Success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteSavedAccessCodes(): CompletionResult<Unit> {
|
private suspend fun deleteSavedAccessCodes(): CompletionResult<Unit> {
|
||||||
return tangemSdkManager.clearSavedUserCodes()
|
return tangemSdkManager.clearSavedUserCodes()
|
||||||
.doOnSuccess {
|
.doOnSuccess {
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
||||||
return when (action) {
|
return when (action) {
|
||||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
|
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
|
||||||
appSettingsState = when (action.setting) {
|
appSettingsState = when (action.setting) {
|
||||||
AppSetting.SaveWallets -> state.appSettingsState.copy(
|
|
||||||
isInProgress = true,
|
|
||||||
saveWallets = action.enable,
|
|
||||||
)
|
|
||||||
AppSetting.SaveAccessCode -> state.appSettingsState.copy(
|
|
||||||
isInProgress = true,
|
|
||||||
saveWallets = true, // User can't enable access codes saving without wallets saving
|
|
||||||
saveAccessCodes = action.enable,
|
|
||||||
)
|
|
||||||
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
|
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
|
||||||
isInProgress = true,
|
isInProgress = true,
|
||||||
requireAccessCode = action.enable,
|
requireAccessCode = action.enable,
|
||||||
|
|
@ -64,14 +55,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
||||||
)
|
)
|
||||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy(
|
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy(
|
||||||
appSettingsState = when (action.setting) {
|
appSettingsState = when (action.setting) {
|
||||||
AppSetting.SaveWallets -> state.appSettingsState.copy(
|
|
||||||
isInProgress = false,
|
|
||||||
saveWallets = action.prevState,
|
|
||||||
)
|
|
||||||
AppSetting.SaveAccessCode -> state.appSettingsState.copy(
|
|
||||||
isInProgress = false,
|
|
||||||
saveAccessCodes = action.prevState,
|
|
||||||
)
|
|
||||||
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
|
AppSetting.RequireAccessCode -> state.appSettingsState.copy(
|
||||||
isInProgress = false,
|
isInProgress = false,
|
||||||
requireAccessCode = action.prevState,
|
requireAccessCode = action.prevState,
|
||||||
|
|
@ -105,9 +88,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
||||||
// state should be copied to avoid concurrent modifications from different sources
|
// state should be copied to avoid concurrent modifications from different sources
|
||||||
is DetailsAction.AppSettings.Prepare -> state.copy(
|
is DetailsAction.AppSettings.Prepare -> state.copy(
|
||||||
appSettingsState = state.appSettingsState.copy(
|
appSettingsState = state.appSettingsState.copy(
|
||||||
saveWallets = action.state.saveWallets,
|
|
||||||
saveAccessCodes = action.state.saveAccessCodes,
|
|
||||||
isBiometricsAvailable = action.state.isBiometricsAvailable,
|
|
||||||
isHidingEnabled = action.state.isHidingEnabled,
|
isHidingEnabled = action.state.isHidingEnabled,
|
||||||
selectedAppCurrency = action.state.selectedAppCurrency,
|
selectedAppCurrency = action.state.selectedAppCurrency,
|
||||||
selectedThemeMode = action.state.selectedThemeMode,
|
selectedThemeMode = action.state.selectedThemeMode,
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,6 @@ data class DetailsState(
|
||||||
|
|
||||||
@Suppress("BooleanPropertyNaming")
|
@Suppress("BooleanPropertyNaming")
|
||||||
data class AppSettingsState(
|
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 requireAccessCode: Boolean = false,
|
||||||
val useBiometricAuthentication: Boolean = false,
|
val useBiometricAuthentication: Boolean = false,
|
||||||
val needEnrollBiometrics: Boolean = false,
|
val needEnrollBiometrics: Boolean = false,
|
||||||
|
|
@ -32,5 +26,5 @@ data class AppSettingsState(
|
||||||
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
||||||
|
|
||||||
enum class AppSetting {
|
enum class AppSetting {
|
||||||
SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication,
|
RequireAccessCode, BiometricAuthentication,
|
||||||
}
|
}
|
||||||
|
|
@ -9,26 +9,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
internal class AppSettingsDialogsFactory {
|
internal class AppSettingsDialogsFactory {
|
||||||
|
|
||||||
fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
|
|
||||||
return Dialog.Alert(
|
|
||||||
title = resourceReference(R.string.common_attention),
|
|
||||||
description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message),
|
|
||||||
confirmText = resourceReference(R.string.common_delete),
|
|
||||||
onConfirm = onDelete,
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert {
|
|
||||||
return Dialog.Alert(
|
|
||||||
title = resourceReference(R.string.common_attention),
|
|
||||||
description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message),
|
|
||||||
confirmText = resourceReference(R.string.common_delete),
|
|
||||||
onConfirm = onDelete,
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createThemeModeSelectorDialog(
|
fun createThemeModeSelectorDialog(
|
||||||
selectedModeIndex: Int,
|
selectedModeIndex: Int,
|
||||||
onSelect: (AppThemeMode) -> Unit,
|
onSelect: (AppThemeMode) -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -19,21 +19,6 @@ internal class AppSettingsItemsFactory {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createSaveWalletsSwitch(
|
|
||||||
isChecked: Boolean,
|
|
||||||
isEnabled: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
): Item.Switch {
|
|
||||||
return Item.Switch(
|
|
||||||
id = ID_SAVE_WALLETS_SWITCH,
|
|
||||||
title = resourceReference(R.string.app_settings_saved_wallet),
|
|
||||||
description = resourceReference(R.string.app_settings_saved_wallet_footer),
|
|
||||||
isEnabled = isEnabled,
|
|
||||||
isChecked = isChecked,
|
|
||||||
onCheckedChange = onCheckedChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createUseBiometricsSwitch(
|
fun createUseBiometricsSwitch(
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
isEnabled: Boolean,
|
isEnabled: Boolean,
|
||||||
|
|
@ -127,7 +112,6 @@ internal class AppSettingsItemsFactory {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ID_ENROLL_BIOMETRICS_CARD = "enroll_biometrics_card"
|
const val ID_ENROLL_BIOMETRICS_CARD = "enroll_biometrics_card"
|
||||||
const val ID_SAVE_WALLETS_SWITCH = "save_wallets_switch"
|
|
||||||
const val ID_SAVE_ACCESS_CODES_SWITCH = "save_access_codes_switch"
|
const val ID_SAVE_ACCESS_CODES_SWITCH = "save_access_codes_switch"
|
||||||
const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch"
|
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_APP_CURRENCY_BUTTON = "select_app_currency_button"
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,6 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide
|
||||||
val items = persistentListOf(
|
val items = persistentListOf(
|
||||||
itemsFactory.createEnrollBiometricsCard {},
|
itemsFactory.createEnrollBiometricsCard {},
|
||||||
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
|
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
|
||||||
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
|
|
||||||
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
|
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||||
itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> },
|
itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||||
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
|
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,12 @@ private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dia
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Alert>(
|
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog>(
|
||||||
collection = buildList {
|
collection = buildList {
|
||||||
val dialogsFactory = AppSettingsDialogsFactory()
|
val dialogsFactory = AppSettingsDialogsFactory()
|
||||||
|
|
||||||
add(dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}))
|
add(dialogsFactory.createThemeModeSelectorDialog(selectedModeIndex = 0, onSelect = {}, onDismiss = {}))
|
||||||
add(dialogsFactory.createDeleteSavedWalletsAlert({}, {}))
|
add(dialogsFactory.createDisableBiometricAuthenticationAlert(onDisable = {}, onDismiss = {}))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// endregion Preview
|
// endregion Preview
|
||||||
|
|
@ -14,10 +14,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
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.AnalyticsParam
|
||||||
import com.tangem.tap.common.analytics.events.Settings
|
import com.tangem.tap.common.analytics.events.Settings
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
|
|
@ -52,14 +49,11 @@ internal class AppSettingsModel @Inject constructor(
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val appCurrencyRepository: AppCurrencyRepository,
|
private val appCurrencyRepository: AppCurrencyRepository,
|
||||||
private val walletsRepository: WalletsRepository,
|
private val walletsRepository: WalletsRepository,
|
||||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val balanceHidingRepository: BalanceHidingRepository,
|
private val balanceHidingRepository: BalanceHidingRepository,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val appThemeModeRepository: AppThemeModeRepository,
|
private val appThemeModeRepository: AppThemeModeRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
|
||||||
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
|
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
private val uiMessageSender: UiMessageSender,
|
private val uiMessageSender: UiMessageSender,
|
||||||
) : Model(), StoreSubscriber<DetailsState> {
|
) : Model(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
|
|
@ -120,47 +114,25 @@ internal class AppSettingsModel @Inject constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
val canUseBiometrics =
|
||||||
val canUseBiometrics =
|
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
|
||||||
!state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets
|
|
||||||
|
|
||||||
add(
|
add(
|
||||||
itemsFactory.createUseBiometricsSwitch(
|
itemsFactory.createUseBiometricsSwitch(
|
||||||
isChecked = state.useBiometricAuthentication,
|
isChecked = state.useBiometricAuthentication,
|
||||||
isEnabled = canUseBiometrics,
|
isEnabled = canUseBiometrics,
|
||||||
onCheckedChange = ::onBiometricAuthenticationToggled,
|
onCheckedChange = ::onBiometricAuthenticationToggled,
|
||||||
onDisabledClick = ::onBiometricAuthenticationDisabledClicked,
|
onDisabledClick = ::onBiometricAuthenticationDisabledClicked,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
add(
|
add(
|
||||||
itemsFactory.createRequireAccessCodeSwitch(
|
itemsFactory.createRequireAccessCodeSwitch(
|
||||||
isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
|
isChecked = state.requireAccessCode || !state.useBiometricAuthentication,
|
||||||
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
|
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
|
||||||
onCheckedChange = ::onRequireAccessCodeToggled,
|
onCheckedChange = ::onRequireAccessCodeToggled,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
if (state.isBiometricsAvailable) {
|
|
||||||
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
|
|
||||||
|
|
||||||
add(
|
|
||||||
itemsFactory.createSaveWalletsSwitch(
|
|
||||||
isChecked = state.saveWallets,
|
|
||||||
isEnabled = canUseBiometrics,
|
|
||||||
onCheckedChange = ::onSaveWalletsToggled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
add(
|
|
||||||
itemsFactory.createSaveAccessCodeSwitch(
|
|
||||||
isChecked = state.saveAccessCodes,
|
|
||||||
isEnabled = canUseBiometrics,
|
|
||||||
onCheckedChange = ::onSaveAccessCodesToggled,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add(
|
add(
|
||||||
itemsFactory.createFlipToHideBalanceSwitch(
|
itemsFactory.createFlipToHideBalanceSwitch(
|
||||||
|
|
@ -261,42 +233,6 @@ internal class AppSettingsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSaveWalletsToggled(isChecked: Boolean) {
|
|
||||||
if (isChecked) {
|
|
||||||
onSettingsToggled(AppSetting.SaveWallets, enable = true)
|
|
||||||
} else {
|
|
||||||
updateContentState {
|
|
||||||
copy(
|
|
||||||
dialog = dialogsFactory.createDeleteSavedWalletsAlert(
|
|
||||||
onDelete = {
|
|
||||||
onSettingsToggled(AppSetting.SaveWallets, enable = false)
|
|
||||||
dismissDialog()
|
|
||||||
},
|
|
||||||
onDismiss = ::dismissDialog,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSaveAccessCodesToggled(isChecked: Boolean) {
|
|
||||||
if (isChecked) {
|
|
||||||
onSettingsToggled(AppSetting.SaveAccessCode, enable = true)
|
|
||||||
} else {
|
|
||||||
updateContentState {
|
|
||||||
copy(
|
|
||||||
dialog = dialogsFactory.createDeleteSavedAccessCodesAlert(
|
|
||||||
onDelete = {
|
|
||||||
onSettingsToggled(AppSetting.SaveAccessCode, enable = false)
|
|
||||||
dismissDialog()
|
|
||||||
},
|
|
||||||
onDismiss = ::dismissDialog,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
|
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
|
||||||
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
||||||
}
|
}
|
||||||
|
|
@ -326,9 +262,6 @@ internal class AppSettingsModel @Inject constructor(
|
||||||
|
|
||||||
private fun bootstrapBiometricsUpdates() = modelScope.launch {
|
private fun bootstrapBiometricsUpdates() = modelScope.launch {
|
||||||
val state = AppSettingsState(
|
val state = AppSettingsState(
|
||||||
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
|
|
||||||
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
|
|
||||||
isBiometricsAvailable = canUseBiometryUseCase(),
|
|
||||||
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
|
useBiometricAuthentication = walletsRepository.useBiometricAuthentication(),
|
||||||
requireAccessCode = walletsRepository.requireAccessCode(),
|
requireAccessCode = walletsRepository.requireAccessCode(),
|
||||||
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
|
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,9 @@ import com.tangem.domain.card.ResetCardUseCase
|
||||||
import com.tangem.domain.card.ResetCardUserCodeParams
|
import com.tangem.domain.card.ResetCardUserCodeParams
|
||||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.models.wallet.requireColdWallet
|
import com.tangem.domain.models.wallet.requireColdWallet
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.legacy.asLockable
|
|
||||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.tap.common.analytics.events.Settings
|
import com.tangem.tap.common.analytics.events.Settings
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||||
|
|
@ -52,10 +49,8 @@ internal class ResetCardModel @Inject constructor(
|
||||||
private val resetCardUseCase: ResetCardUseCase,
|
private val resetCardUseCase: ResetCardUseCase,
|
||||||
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
|
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
|
||||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val cardSettingsInteractor: CardSettingsInteractor,
|
private val cardSettingsInteractor: CardSettingsInteractor,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<ResetCardComponent.Params>()
|
private val params = paramsContainer.require<ResetCardComponent.Params>()
|
||||||
|
|
@ -277,16 +272,7 @@ internal class ResetCardModel @Inject constructor(
|
||||||
if (newSelectedWallet != null) {
|
if (newSelectedWallet != null) {
|
||||||
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
||||||
} else {
|
} else {
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
|
|
||||||
} else {
|
|
||||||
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked }.isSuccess
|
|
||||||
if (isLocked && userWalletsListManager.hasUserWallets) {
|
|
||||||
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
|
|
||||||
} else {
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.component
|
|
||||||
|
|
||||||
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.tap.features.welcome.model.WelcomeModel
|
|
||||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
|
|
||||||
internal class DefaultWelcomeComponent @AssistedInject constructor(
|
|
||||||
@Assisted context: AppComponentContext,
|
|
||||||
@Assisted params: WelcomeComponent.Params,
|
|
||||||
) : WelcomeComponent, AppComponentContext by context {
|
|
||||||
|
|
||||||
private val model: WelcomeModel = getOrCreateModel(params)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Content(modifier: Modifier) {
|
|
||||||
val state by model.state.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
WelcomeScreen(
|
|
||||||
modifier = modifier,
|
|
||||||
state = state,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory : WelcomeComponent.Factory {
|
|
||||||
override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.component
|
|
||||||
|
|
||||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
|
||||||
import com.tangem.core.decompose.factory.ComponentFactory
|
|
||||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
|
||||||
|
|
||||||
interface WelcomeComponent : ComposableContentComponent {
|
|
||||||
|
|
||||||
data class Params(
|
|
||||||
val launchMode: InitScreenLaunchMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Factory : ComponentFactory<Params, WelcomeComponent>
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.component.impl
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
|
||||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
|
||||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
|
|
||||||
|
|
||||||
internal class PreviewWelcomeComponent(
|
|
||||||
private val initialState: WelcomeScreenState = WelcomeScreenState(),
|
|
||||||
) : WelcomeComponent {
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Content(modifier: Modifier) {
|
|
||||||
WelcomeScreen(
|
|
||||||
modifier = modifier,
|
|
||||||
state = initialState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.di
|
|
||||||
|
|
||||||
import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent
|
|
||||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
|
||||||
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 ComponentModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Singleton
|
|
||||||
fun bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.di
|
|
||||||
|
|
||||||
import com.tangem.core.decompose.di.ModelComponent
|
|
||||||
import com.tangem.core.decompose.model.Model
|
|
||||||
import com.tangem.tap.features.welcome.model.WelcomeModel
|
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.multibindings.ClassKey
|
|
||||||
import dagger.multibindings.IntoMap
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(ModelComponent::class)
|
|
||||||
internal interface ModelModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@ClassKey(WelcomeModel::class)
|
|
||||||
fun bindWelcomeModel(model: WelcomeModel): Model
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.model
|
|
||||||
|
|
||||||
import com.tangem.common.core.TangemError
|
|
||||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
|
||||||
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.finisher.AppFinisher
|
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
|
||||||
import com.tangem.tap.common.analytics.events.SignIn
|
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
|
||||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
|
||||||
import com.tangem.tap.features.welcome.redux.WelcomeState
|
|
||||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
|
||||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
|
||||||
import com.tangem.tap.store
|
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import org.rekotlin.StoreSubscriber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
// FIXME: Remove redux: [REDACTED_JIRA]
|
|
||||||
@ModelScoped
|
|
||||||
internal class WelcomeModel @Inject constructor(
|
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
|
||||||
private val appFinisher: AppFinisher,
|
|
||||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
|
||||||
paramsContainer: ParamsContainer,
|
|
||||||
) : Model(), StoreSubscriber<WelcomeState> {
|
|
||||||
|
|
||||||
private val params: WelcomeComponent.Params = paramsContainer.require()
|
|
||||||
private val initialState: WelcomeScreenState = WelcomeScreenState(
|
|
||||||
onPopBack = appFinisher::finish,
|
|
||||||
onUnlockClick = this::unlockWallets,
|
|
||||||
onScanCardClick = this::scanCard,
|
|
||||||
onCloseError = this::closeError,
|
|
||||||
)
|
|
||||||
|
|
||||||
val state: MutableStateFlow<WelcomeScreenState> = MutableStateFlow(initialState)
|
|
||||||
|
|
||||||
init {
|
|
||||||
subscribeToStoreChanges()
|
|
||||||
initGlobalState()
|
|
||||||
|
|
||||||
val welcomeAction = when (params.launchMode) {
|
|
||||||
is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard
|
|
||||||
is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics
|
|
||||||
}
|
|
||||||
|
|
||||||
store.dispatch(welcomeAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unlockWallets() {
|
|
||||||
analyticsEventsHandler.send(SignIn.ButtonBiometricSignIn())
|
|
||||||
store.dispatch(WelcomeAction.ProceedWithBiometrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scanCard() {
|
|
||||||
analyticsEventsHandler.send(SignIn.ButtonCardSignIn())
|
|
||||||
store.dispatch(WelcomeAction.ProceedWithCard)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun closeError() {
|
|
||||||
store.dispatch(WelcomeAction.CloseError)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newState(state: WelcomeState) {
|
|
||||||
val warning = createWarningIfNeeded(state.error)
|
|
||||||
|
|
||||||
this.state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
isUnlockWithBiometricsProgressVisible = state.isUnlockWithBiometricsInProgress,
|
|
||||||
isUnlockWithCardProgressVisible = state.isUnlockWithCardInProgress,
|
|
||||||
warning = warning,
|
|
||||||
error = state.error
|
|
||||||
?.takeIf { !it.silent && warning == null }
|
|
||||||
?.let { error ->
|
|
||||||
val messageResId = error.messageResId
|
|
||||||
if (messageResId != null) {
|
|
||||||
TextReference.Res(messageResId)
|
|
||||||
} else {
|
|
||||||
TextReference.Str(error.customMessage)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
store.unsubscribe(subscriber = this)
|
|
||||||
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createWarningIfNeeded(error: TangemError?): WarningModel? {
|
|
||||||
return when (error) {
|
|
||||||
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
|
|
||||||
isPermanent = error.isPermanent,
|
|
||||||
onDismiss = this::dismissWarning,
|
|
||||||
)
|
|
||||||
is UserWalletsListError.AllKeysInvalidated,
|
|
||||||
is UserWalletsListError.NoUserWalletSelected,
|
|
||||||
-> WarningModel.KeyInvalidatedWarning(
|
|
||||||
onDismiss = this::dismissWarning,
|
|
||||||
)
|
|
||||||
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
|
|
||||||
onDismiss = this::clearUserWallets,
|
|
||||||
)
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dismissWarning() {
|
|
||||||
state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
warning = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
closeError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearUserWallets() {
|
|
||||||
store.dispatch(WelcomeAction.ClearUserWallets)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun subscribeToStoreChanges() {
|
|
||||||
store.subscribe(this) { appState ->
|
|
||||||
appState.skip { old, new -> old.welcomeState == new.welcomeState }
|
|
||||||
.select { it.welcomeState }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initGlobalState() {
|
|
||||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.redux
|
|
||||||
|
|
||||||
import com.tangem.common.core.TangemError
|
|
||||||
import org.rekotlin.Action
|
|
||||||
|
|
||||||
internal sealed interface WelcomeAction : Action {
|
|
||||||
|
|
||||||
data object ProceedWithBiometrics : WelcomeAction {
|
|
||||||
object Success : WelcomeAction
|
|
||||||
data class Error(val error: TangemError) : WelcomeAction
|
|
||||||
}
|
|
||||||
|
|
||||||
object ProceedWithCard : WelcomeAction {
|
|
||||||
object Success : WelcomeAction
|
|
||||||
data class Error(val error: TangemError) : WelcomeAction
|
|
||||||
data class ChangeProgress(val isProgress: Boolean) : WelcomeAction
|
|
||||||
}
|
|
||||||
|
|
||||||
object CloseError : WelcomeAction
|
|
||||||
|
|
||||||
object ClearUserWallets : WelcomeAction
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.redux
|
|
||||||
|
|
||||||
import com.tangem.common.core.TangemSdkError
|
|
||||||
import com.tangem.common.doOnFailure
|
|
||||||
import com.tangem.common.doOnResult
|
|
||||||
import com.tangem.common.doOnSuccess
|
|
||||||
import com.tangem.common.flatMap
|
|
||||||
import com.tangem.common.routing.AppRoute
|
|
||||||
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.extensions.*
|
|
||||||
import com.tangem.tap.common.redux.AppState
|
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.rekotlin.Middleware
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
internal class WelcomeMiddleware {
|
|
||||||
val middleware: Middleware<AppState> = { _, appStateProvider ->
|
|
||||||
{ next ->
|
|
||||||
{ action ->
|
|
||||||
val appState = appStateProvider()
|
|
||||||
if (action is WelcomeAction && appState != null) {
|
|
||||||
handleAction(action)
|
|
||||||
}
|
|
||||||
next(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAction(action: WelcomeAction) {
|
|
||||||
mainScope.launch {
|
|
||||||
when (action) {
|
|
||||||
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics()
|
|
||||||
is WelcomeAction.ProceedWithCard -> proceedWithCard()
|
|
||||||
is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving()
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun proceedWithBiometrics() {
|
|
||||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
|
||||||
userWalletsListManager.unlockIfLockable(type = UnlockType.ANY)
|
|
||||||
.doOnFailure { error ->
|
|
||||||
Timber.e(error, "Unable to unlock user wallets with biometrics")
|
|
||||||
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error))
|
|
||||||
}
|
|
||||||
.doOnSuccess { selectedUserWallet ->
|
|
||||||
sendSignedInAnalyticsEvent(
|
|
||||||
userWallet = selectedUserWallet,
|
|
||||||
signInType = Basic.SignedInLegacy.SignInType.Biometric,
|
|
||||||
)
|
|
||||||
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
|
|
||||||
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success)
|
|
||||||
store.onUserWalletSelected(userWallet = selectedUserWallet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun proceedWithCard() {
|
|
||||||
scanCardInternal { scanResponse ->
|
|
||||||
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
|
|
||||||
|
|
||||||
val userWallet = userWalletBuilder.build() ?: return@scanCardInternal
|
|
||||||
|
|
||||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
|
||||||
userWalletsListManager.save(userWallet, canOverride = true)
|
|
||||||
.doOnFailure { error ->
|
|
||||||
Timber.e(error, "Unable to save user wallet")
|
|
||||||
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error))
|
|
||||||
}
|
|
||||||
.doOnSuccess {
|
|
||||||
sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedInLegacy.SignInType.Card)
|
|
||||||
|
|
||||||
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
|
|
||||||
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
|
|
||||||
store.onUserWalletSelected(userWallet = userWallet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedInLegacy.SignInType) {
|
|
||||||
if (userWallet !is UserWallet.Cold) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val scanResponse = userWallet.scanResponse
|
|
||||||
val currency = ParamCardCurrencyConverter().convert(
|
|
||||||
value = scanResponse.cardTypesResolver,
|
|
||||||
)
|
|
||||||
|
|
||||||
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
|
|
||||||
trackingContextProxy.addContext(scanResponse)
|
|
||||||
|
|
||||||
if (currency != null) {
|
|
||||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
|
||||||
|
|
||||||
Analytics.send(
|
|
||||||
event = Basic.SignedInLegacy(
|
|
||||||
currency = currency,
|
|
||||||
batch = scanResponse.card.batchId,
|
|
||||||
signInType = signInType,
|
|
||||||
walletsCount = userWalletsListManager.walletsCount.toString(),
|
|
||||||
isImported = userWallet.isImported,
|
|
||||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun disableUserWalletsSaving() {
|
|
||||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
|
||||||
userWalletsListManager.clear()
|
|
||||||
.flatMap { tangemSdkManager.clearSavedUserCodes() }
|
|
||||||
.doOnFailure { e ->
|
|
||||||
Timber.e(e, "Unable to clear user wallets")
|
|
||||||
}
|
|
||||||
.doOnResult {
|
|
||||||
store.dispatchWithMain(WelcomeAction.CloseError)
|
|
||||||
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) {
|
|
||||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
|
||||||
|
|
||||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
|
||||||
isBiometricsRequestPolicy = shouldSaveAccessCodes,
|
|
||||||
)
|
|
||||||
|
|
||||||
store.inject(DaggerGraphState::scanCardProcessor).scan(
|
|
||||||
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
|
|
||||||
onSuccess = { scanResponse ->
|
|
||||||
scope.launch { onCardScanned(scanResponse) }
|
|
||||||
},
|
|
||||||
onFailure = { error ->
|
|
||||||
when (error) {
|
|
||||||
is TangemSdkError.ExceptionError -> {
|
|
||||||
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onProgressStateChange = {
|
|
||||||
store.dispatchWithMain(WelcomeAction.ProceedWithCard.ChangeProgress(it))
|
|
||||||
},
|
|
||||||
onWalletNotCreated = {
|
|
||||||
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.redux
|
|
||||||
|
|
||||||
import com.tangem.tap.common.redux.AppState
|
|
||||||
import org.rekotlin.Action
|
|
||||||
|
|
||||||
internal object WelcomeReducer {
|
|
||||||
fun reduce(action: Action, state: AppState): WelcomeState {
|
|
||||||
return if (action is WelcomeAction) {
|
|
||||||
internalReduce(action, state.welcomeState)
|
|
||||||
} else {
|
|
||||||
state.welcomeState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState {
|
|
||||||
return when (action) {
|
|
||||||
is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true)
|
|
||||||
is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true)
|
|
||||||
is WelcomeAction.ProceedWithBiometrics.Error -> state.copy(
|
|
||||||
error = action.error,
|
|
||||||
isUnlockWithBiometricsInProgress = false,
|
|
||||||
)
|
|
||||||
is WelcomeAction.ProceedWithCard.Error -> state.copy(
|
|
||||||
error = action.error,
|
|
||||||
isUnlockWithCardInProgress = false,
|
|
||||||
)
|
|
||||||
is WelcomeAction.ProceedWithCard.ChangeProgress -> state.copy(
|
|
||||||
isUnlockWithCardInProgress = action.isProgress,
|
|
||||||
)
|
|
||||||
is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false)
|
|
||||||
is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false)
|
|
||||||
is WelcomeAction.CloseError -> state.copy(error = null)
|
|
||||||
else -> state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.redux
|
|
||||||
|
|
||||||
import com.tangem.common.core.TangemError
|
|
||||||
import org.rekotlin.StateType
|
|
||||||
|
|
||||||
data class WelcomeState(
|
|
||||||
val isUnlockWithBiometricsInProgress: Boolean = false,
|
|
||||||
val isUnlockWithCardInProgress: Boolean = false,
|
|
||||||
val error: TangemError? = null,
|
|
||||||
) : StateType
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.ui
|
|
||||||
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
|
||||||
|
|
||||||
internal data class WelcomeScreenState(
|
|
||||||
val onPopBack: () -> Unit = {},
|
|
||||||
val isUnlockWithBiometricsProgressVisible: Boolean = false,
|
|
||||||
val isUnlockWithCardProgressVisible: Boolean = false,
|
|
||||||
val warning: WarningModel? = null,
|
|
||||||
val error: TextReference? = null,
|
|
||||||
val onUnlockClick: () -> Unit = {},
|
|
||||||
val onScanCardClick: () -> Unit = {},
|
|
||||||
val onCloseError: () -> Unit = {},
|
|
||||||
)
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.ui.components
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.tangem.core.ui.components.BasicDialog
|
|
||||||
import com.tangem.core.ui.components.DialogButtonUM
|
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
|
||||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
|
||||||
import com.tangem.wallet.R
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun WarningDialog(warning: WarningModel?) {
|
|
||||||
when (warning) {
|
|
||||||
null -> Unit
|
|
||||||
is WarningModel.BiometricsLockoutWarning -> {
|
|
||||||
BasicDialog(
|
|
||||||
title = stringResourceSafe(id = R.string.biometric_lockout_warning_title),
|
|
||||||
message = stringResourceSafe(
|
|
||||||
id = if (warning.isPermanent) {
|
|
||||||
R.string.biometric_lockout_permanent_warning_description
|
|
||||||
} else {
|
|
||||||
R.string.biometric_lockout_warning_description
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onDismissDialog = warning.onDismiss,
|
|
||||||
confirmButton = DialogButtonUM(
|
|
||||||
title = stringResourceSafe(id = R.string.common_ok),
|
|
||||||
onClick = warning.onDismiss,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is WarningModel.KeyInvalidatedWarning -> {
|
|
||||||
BasicDialog(
|
|
||||||
title = stringResourceSafe(id = R.string.common_attention),
|
|
||||||
message = stringResourceSafe(id = R.string.key_invalidated_warning_description),
|
|
||||||
onDismissDialog = warning.onDismiss,
|
|
||||||
confirmButton = DialogButtonUM(
|
|
||||||
title = stringResourceSafe(id = R.string.common_ok),
|
|
||||||
onClick = warning.onDismiss,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is WarningModel.BiometricsDisabledWarning -> {
|
|
||||||
BasicDialog(
|
|
||||||
title = stringResourceSafe(id = R.string.common_warning),
|
|
||||||
message = stringResourceSafe(id = R.string.biometric_unavailable_warning),
|
|
||||||
onDismissDialog = warning.onDismiss,
|
|
||||||
isDismissable = false,
|
|
||||||
confirmButton = DialogButtonUM(
|
|
||||||
title = stringResourceSafe(id = R.string.common_ok),
|
|
||||||
onClick = warning.onDismiss,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Preview
|
|
||||||
@Composable
|
|
||||||
private fun BiometricsLockoutDialogSample(modifier: Modifier = Modifier) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
WarningDialog(
|
|
||||||
warning = WarningModel.BiometricsLockoutWarning(
|
|
||||||
isPermanent = false,
|
|
||||||
onDismiss = {},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
private fun BiometricsLockoutDialogPreview() {
|
|
||||||
TangemThemePreview {
|
|
||||||
BiometricsLockoutDialogSample()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BiometricsLockoutDialog_Permanent_Sample(modifier: Modifier = Modifier) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
WarningDialog(
|
|
||||||
warning = WarningModel.BiometricsLockoutWarning(
|
|
||||||
isPermanent = true,
|
|
||||||
onDismiss = {},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
private fun BiometricsLockoutDialog_Permanent_Preview() {
|
|
||||||
TangemThemePreview {
|
|
||||||
BiometricsLockoutDialog_Permanent_Sample()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
WarningDialog(warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
private fun KeyInvalidatedWarningPreview() {
|
|
||||||
TangemThemePreview {
|
|
||||||
KeyInvalidatedWarningSample()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) {
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onDismiss = {}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
private fun BiometricDisabledWarningPreview() {
|
|
||||||
TangemThemePreview {
|
|
||||||
BiometricDisabledWarningSample()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion Preview
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.ui.components
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
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.PreviewParameterProvider
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
|
||||||
import com.tangem.core.ui.extensions.resolveReference
|
|
||||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
|
||||||
import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent
|
|
||||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
|
||||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modifier) {
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
|
||||||
val warning by rememberUpdatedState(newValue = state.warning)
|
|
||||||
|
|
||||||
BackHandler(onBack = state.onPopBack)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.background(TangemTheme.colors.background.primary)
|
|
||||||
.systemBarsPadding(),
|
|
||||||
) {
|
|
||||||
WelcomeScreenContent(
|
|
||||||
showUnlockProgress = state.isUnlockWithBiometricsProgressVisible,
|
|
||||||
showScanCardProgress = state.isUnlockWithCardProgressVisible,
|
|
||||||
onUnlockClick = state.onUnlockClick,
|
|
||||||
onScanCardClick = state.onScanCardClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
SnackbarHost(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
hostState = snackbarHostState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
WarningDialog(warning)
|
|
||||||
|
|
||||||
LaunchedEffect(errorMessage, state.onCloseError) {
|
|
||||||
errorMessage?.let { message ->
|
|
||||||
snackbarHostState.showSnackbar(message)
|
|
||||||
state.onCloseError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Preview
|
|
||||||
@Composable
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
private fun Preview_WelcomeScreen(
|
|
||||||
@PreviewParameter(WelcomeComponentPreviewProvider::class) component: WelcomeComponent,
|
|
||||||
) {
|
|
||||||
TangemThemePreview {
|
|
||||||
component.Content(Modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class WelcomeComponentPreviewProvider : PreviewParameterProvider<WelcomeComponent> {
|
|
||||||
override val values: Sequence<WelcomeComponent>
|
|
||||||
get() = sequenceOf(
|
|
||||||
PreviewWelcomeComponent(),
|
|
||||||
PreviewWelcomeComponent(
|
|
||||||
initialState = WelcomeScreenState(
|
|
||||||
isUnlockWithBiometricsProgressVisible = true,
|
|
||||||
isUnlockWithCardProgressVisible = true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PreviewWelcomeComponent(
|
|
||||||
initialState = WelcomeScreenState(
|
|
||||||
error = TextReference.Str(value = "Error"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PreviewWelcomeComponent(
|
|
||||||
initialState = WelcomeScreenState(
|
|
||||||
warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// endregion Preview
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.ui.components
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
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.components.*
|
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
|
||||||
import com.tangem.wallet.R
|
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
@Composable
|
|
||||||
internal fun WelcomeScreenContent(
|
|
||||||
showUnlockProgress: Boolean,
|
|
||||||
showScanCardProgress: Boolean,
|
|
||||||
onUnlockClick: () -> Unit,
|
|
||||||
onScanCardClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
SpacerHMax()
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier.size(TangemTheme.dimens.size96),
|
|
||||||
painter = painterResource(id = R.drawable.img_tangem_logo_96),
|
|
||||||
tint = TangemTheme.colors.icon.primary1,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
SpacerH32()
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
text = stringResourceSafe(R.string.welcome_unlock_title),
|
|
||||||
style = TangemTheme.typography.h2,
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
SpacerH12()
|
|
||||||
Text(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = TangemTheme.dimens.spacing44)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
text = stringResourceSafe(
|
|
||||||
id = R.string.welcome_unlock_description,
|
|
||||||
stringResourceSafe(id = R.string.common_biometric_authentication),
|
|
||||||
),
|
|
||||||
style = TangemTheme.typography.body1,
|
|
||||||
color = TangemTheme.colors.text.secondary,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
SpacerHMax()
|
|
||||||
SecondaryButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
text = stringResourceSafe(
|
|
||||||
id = R.string.welcome_unlock,
|
|
||||||
stringResourceSafe(id = R.string.common_biometrics),
|
|
||||||
),
|
|
||||||
showProgress = showUnlockProgress,
|
|
||||||
onClick = onUnlockClick,
|
|
||||||
)
|
|
||||||
SpacerH12()
|
|
||||||
PrimaryButtonIconEnd(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
text = stringResourceSafe(R.string.welcome_unlock_card),
|
|
||||||
showProgress = showScanCardProgress,
|
|
||||||
iconResId = R.drawable.ic_tangem_24,
|
|
||||||
onClick = onScanCardClick,
|
|
||||||
)
|
|
||||||
SpacerH16()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Preview
|
|
||||||
@Composable
|
|
||||||
private fun WelcomeScreenContentSample(modifier: Modifier = Modifier) {
|
|
||||||
Box(
|
|
||||||
modifier = modifier
|
|
||||||
.background(TangemTheme.colors.background.primary),
|
|
||||||
) {
|
|
||||||
WelcomeScreenContent(
|
|
||||||
showUnlockProgress = false,
|
|
||||||
showScanCardProgress = false,
|
|
||||||
onUnlockClick = { /* no-op */ },
|
|
||||||
onScanCardClick = { /* no-op */ },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Composable
|
|
||||||
private fun WelcomeScreenContentPreview() {
|
|
||||||
TangemThemePreview {
|
|
||||||
WelcomeScreenContentSample()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion Preview
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package com.tangem.tap.features.welcome.ui.model
|
|
||||||
|
|
||||||
internal sealed interface WarningModel {
|
|
||||||
data class BiometricsLockoutWarning(
|
|
||||||
val isPermanent: Boolean,
|
|
||||||
val onDismiss: () -> Unit,
|
|
||||||
) : WarningModel
|
|
||||||
|
|
||||||
data class KeyInvalidatedWarning(
|
|
||||||
val onDismiss: () -> Unit,
|
|
||||||
) : WarningModel
|
|
||||||
|
|
||||||
data class BiometricsDisabledWarning(
|
|
||||||
val onDismiss: () -> Unit,
|
|
||||||
) : WarningModel
|
|
||||||
}
|
|
||||||
|
|
@ -6,14 +6,11 @@ import com.tangem.datasource.api.common.config.ApiEnvironment
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
|
|
||||||
internal class DefaultAuthProvider(
|
internal class DefaultAuthProvider(
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val shouldUseNewListRepository: Boolean = false,
|
|
||||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||||
) : AuthProvider {
|
) : AuthProvider {
|
||||||
|
|
||||||
|
|
@ -71,18 +68,10 @@ internal class DefaultAuthProvider(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getWallets(): List<UserWallet> {
|
private suspend fun getWallets(): List<UserWallet> {
|
||||||
return if (shouldUseNewListRepository) {
|
return userWalletsListRepository.userWalletsSync()
|
||||||
userWalletsListRepository.userWalletsSync()
|
|
||||||
} else {
|
|
||||||
userWalletsListManager.userWalletsSync
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSelectedWallet(): UserWallet? {
|
private suspend fun getSelectedWallet(): UserWallet? {
|
||||||
return if (shouldUseNewListRepository) {
|
return userWalletsListRepository.selectedUserWalletSync()
|
||||||
userWalletsListRepository.selectedUserWalletSync()
|
|
||||||
} else {
|
|
||||||
userWalletsListManager.selectedUserWalletSync
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,16 +3,10 @@ package com.tangem.tap.network.auth.di
|
||||||
import com.tangem.datasource.api.common.AuthProvider
|
import com.tangem.datasource.api.common.AuthProvider
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.lib.auth.ExpressAuthProvider
|
import com.tangem.lib.auth.ExpressAuthProvider
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||||
import com.tangem.tap.network.auth.DefaultAppVersionProvider
|
import com.tangem.tap.network.auth.*
|
||||||
import com.tangem.tap.network.auth.DefaultAuthProvider
|
|
||||||
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
|
|
||||||
import com.tangem.tap.network.auth.DefaultP2PEthPoolAuthProvider
|
|
||||||
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
|
|
||||||
import com.tangem.utils.version.AppVersionProvider
|
import com.tangem.utils.version.AppVersionProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -27,15 +21,11 @@ internal class AuthModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAuthProvider(
|
fun provideAuthProvider(
|
||||||
userWalletsListManager: UserWalletsListManager,
|
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
environmentConfigStorage: EnvironmentConfigStorage,
|
environmentConfigStorage: EnvironmentConfigStorage,
|
||||||
): AuthProvider {
|
): AuthProvider {
|
||||||
return DefaultAuthProvider(
|
return DefaultAuthProvider(
|
||||||
userWalletsListManager = userWalletsListManager,
|
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
shouldUseNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled,
|
|
||||||
environmentConfigStorage = environmentConfigStorage,
|
environmentConfigStorage = environmentConfigStorage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -163,4 +163,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
|
||||||
Linea, LineaTestnet -> null
|
Linea, LineaTestnet -> null
|
||||||
ArbitrumNova -> null
|
ArbitrumNova -> null
|
||||||
Plasma, PlasmaTestnet -> null
|
Plasma, PlasmaTestnet -> null
|
||||||
|
Monad, MonadTestnet -> null
|
||||||
}
|
}
|
||||||
|
|
@ -31,9 +31,7 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|
||||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||||
import com.tangem.hot.sdk.TangemHotSdk
|
import com.tangem.hot.sdk.TangemHotSdk
|
||||||
import com.tangem.operations.attestation.CardArtworksProvider
|
import com.tangem.operations.attestation.CardArtworksProvider
|
||||||
|
|
@ -53,7 +51,6 @@ data class DaggerGraphState(
|
||||||
val appThemeModeRepository: AppThemeModeRepository? = null,
|
val appThemeModeRepository: AppThemeModeRepository? = null,
|
||||||
val balanceHidingRepository: BalanceHidingRepository? = null,
|
val balanceHidingRepository: BalanceHidingRepository? = null,
|
||||||
val walletsRepository: WalletsRepository? = null,
|
val walletsRepository: WalletsRepository? = null,
|
||||||
val generalUserWalletsListManager: UserWalletsListManager? = null,
|
|
||||||
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
|
val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null,
|
||||||
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
|
val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null,
|
||||||
val cardRepository: CardRepository? = null,
|
val cardRepository: CardRepository? = null,
|
||||||
|
|
@ -78,7 +75,6 @@ data class DaggerGraphState(
|
||||||
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
|
val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null,
|
||||||
val userTokensResponseStore: UserTokensResponseStore? = null,
|
val userTokensResponseStore: UserTokensResponseStore? = null,
|
||||||
val userWalletsListRepository: UserWalletsListRepository? = null,
|
val userWalletsListRepository: UserWalletsListRepository? = null,
|
||||||
val hotWalletFeatureToggles: HotWalletFeatureToggles? = null,
|
|
||||||
val tangemHotSdk: TangemHotSdk? = null,
|
val tangemHotSdk: TangemHotSdk? = null,
|
||||||
val trackingContextProxy: TrackingContextProxy? = null,
|
val trackingContextProxy: TrackingContextProxy? = null,
|
||||||
) : StateType
|
) : StateType
|
||||||
|
|
@ -29,7 +29,6 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.models.wallet.isLocked
|
import com.tangem.domain.models.wallet.isLocked
|
||||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||||
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
||||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
|
||||||
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
|
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
|
||||||
import com.tangem.features.walletconnect.components.WcRoutingComponent
|
import com.tangem.features.walletconnect.components.WcRoutingComponent
|
||||||
import com.tangem.hot.sdk.TangemHotSdk
|
import com.tangem.hot.sdk.TangemHotSdk
|
||||||
|
|
@ -69,7 +68,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val cardRepository: CardRepository,
|
private val cardRepository: CardRepository,
|
||||||
private val onboardingRepository: OnboardingRepository,
|
private val onboardingRepository: OnboardingRepository,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
private val trackingContextProxy: TrackingContextProxy,
|
private val trackingContextProxy: TrackingContextProxy,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||||
|
|
@ -254,16 +252,14 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun trackSignInEvent() {
|
private suspend fun trackSignInEvent() {
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
val userWallets = userWalletsListRepository.userWalletsSync()
|
||||||
val userWallets = userWalletsListRepository.userWalletsSync()
|
val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return
|
||||||
val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return
|
trackingContextProxy.addContext(selectedWallet)
|
||||||
trackingContextProxy.addContext(selectedWallet)
|
analyticsEventHandler.send(
|
||||||
analyticsEventHandler.send(
|
event = Basic.SignedIn(
|
||||||
event = Basic.SignedIn(
|
signInType = Basic.SignedIn.SignInType.NoSecurity,
|
||||||
signInType = Basic.SignedIn.SignInType.NoSecurity,
|
walletsCount = userWallets.size,
|
||||||
walletsCount = userWallets.size,
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +52,6 @@ import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
|
||||||
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
|
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
|
||||||
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
|
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
|
||||||
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
|
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
|
||||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
|
||||||
import com.tangem.tap.routing.component.RoutingComponent.Child
|
import com.tangem.tap.routing.component.RoutingComponent.Child
|
||||||
import dagger.hilt.android.scopes.ActivityScoped
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -76,7 +75,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
|
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
|
||||||
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
|
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
|
||||||
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
|
||||||
private val welcomeComponentFactory: WelcomeComponent.Factory,
|
|
||||||
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
|
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
|
||||||
private val storiesComponentFactory: StoriesComponent.Factory,
|
private val storiesComponentFactory: StoriesComponent.Factory,
|
||||||
private val stakingComponentFactory: StakingComponent.Factory,
|
private val stakingComponentFactory: StakingComponent.Factory,
|
||||||
|
|
@ -118,7 +116,6 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
|
||||||
private val kycComponentFactory: KycComponent.Factory,
|
private val kycComponentFactory: KycComponent.Factory,
|
||||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
|
||||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||||
private val feedFeatureToggle: FeedFeatureToggle,
|
private val feedFeatureToggle: FeedFeatureToggle,
|
||||||
) {
|
) {
|
||||||
|
|
@ -163,21 +160,11 @@ internal class ChildFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppRoute.Welcome -> {
|
is AppRoute.Welcome -> {
|
||||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
createComponentChild(
|
||||||
createComponentChild(
|
context = context,
|
||||||
context = context,
|
params = Unit,
|
||||||
params = Unit,
|
componentFactory = newWelcomeComponentFactory,
|
||||||
componentFactory = newWelcomeComponentFactory,
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
createComponentChild(
|
|
||||||
context = context,
|
|
||||||
params = WelcomeComponent.Params(
|
|
||||||
launchMode = route.launchMode,
|
|
||||||
),
|
|
||||||
componentFactory = welcomeComponentFactory,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is AppRoute.WalletSettings -> {
|
is AppRoute.WalletSettings -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
|
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
package com.tangem.tap.domain.userWalletList.implementation
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth
|
|
||||||
import com.tangem.common.test.domain.card.MockScanResponseFactory
|
|
||||||
import com.tangem.domain.card.configs.GenericCardConfig
|
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
|
||||||
import io.mockk.mockk
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.junit.runners.Parameterized
|
|
||||||
import kotlin.reflect.full.declaredFunctions
|
|
||||||
import kotlin.reflect.jvm.isAccessible
|
|
||||||
|
|
||||||
/**
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
@RunWith(Parameterized::class)
|
|
||||||
internal class BiometricUserWalletsListManagerTest(private val model: Model) {
|
|
||||||
|
|
||||||
private val manager = BiometricUserWalletsListManager(
|
|
||||||
keysRepository = mockk(),
|
|
||||||
publicInformationRepository = mockk(),
|
|
||||||
sensitiveInformationRepository = mockk(),
|
|
||||||
selectedUserWalletRepository = mockk(),
|
|
||||||
dispatcherProvider = mockk(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testFindAvailableUserWallet() {
|
|
||||||
with(model) {
|
|
||||||
val actual = userWallets.findAvailableUserWallet(prevSelectedIndex)
|
|
||||||
|
|
||||||
Truth.assertThat(actual).isEqualTo(newSelectedWallet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<UserWallet>.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? {
|
|
||||||
return manager::class
|
|
||||||
.declaredFunctions
|
|
||||||
.firstOrNull { it.name == "findAvailableUserWallet" }
|
|
||||||
?.apply { isAccessible = true }
|
|
||||||
?.call(manager, this, prevSelectedIndex)
|
|
||||||
as? UserWallet
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Model(
|
|
||||||
val userWallets: List<UserWallet>,
|
|
||||||
val prevSelectedIndex: Int,
|
|
||||||
val newSelectedWallet: UserWallet?,
|
|
||||||
)
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
val userWallet0 = createUserWallet(id = "0", isLocked = false)
|
|
||||||
val lockedUserWallet0 = createUserWallet(id = "0", isLocked = true)
|
|
||||||
|
|
||||||
val userWallet1 = createUserWallet(id = "1", isLocked = false)
|
|
||||||
val lockedUserWallet1 = createUserWallet(id = "1", isLocked = true)
|
|
||||||
|
|
||||||
val userWallet2 = createUserWallet(id = "2", isLocked = false)
|
|
||||||
val lockedUserWallet2 = createUserWallet(id = "2", isLocked = true)
|
|
||||||
|
|
||||||
val userWallet3 = createUserWallet(id = "3", isLocked = false)
|
|
||||||
val lockedUserWallet3 = createUserWallet(id = "3", isLocked = true)
|
|
||||||
|
|
||||||
val unlockedWallets = listOf(userWallet0, userWallet1, userWallet2, userWallet3)
|
|
||||||
val lockedWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Parameterized.Parameters
|
|
||||||
fun data(): Collection<Model> {
|
|
||||||
return listOf(
|
|
||||||
Model(userWallets = emptyList(), prevSelectedIndex = 0, newSelectedWallet = null),
|
|
||||||
*getTestsWithUnlockedWallets().toTypedArray(),
|
|
||||||
*getTestsIfPrevSelectedIndexIs0().toTypedArray(),
|
|
||||||
*getTestsIfPrevSelectedIndexIsLastIndex().toTypedArray(),
|
|
||||||
*getTestsIfNewSelectedIndexIsNearby().toTypedArray(),
|
|
||||||
*getTestsIfNewSelectedIndexIsThroughOne().toTypedArray(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTestsWithUnlockedWallets() = listOf(
|
|
||||||
// [*0*, 1, 2, 3, 4] => delete 0 => [1, 2, 3, 4] => select 1 => [*1*, 2, 3, 4]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 0, newSelectedWallet = userWallet0),
|
|
||||||
// [0, *1*, 2, 3, 4] => delete 1 => [0, 2, 3, 4] => select 2 => [0, *2*, 3, 4]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 1, newSelectedWallet = userWallet1),
|
|
||||||
// [0, 1, *2*, 3, 4] => delete 2 => [0, 1, 3, 4] => select 3 => [0, 1, *3*, 4]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 2, newSelectedWallet = userWallet2),
|
|
||||||
// [0, 1, 2, *3*, 4] => delete 3 => [0, 1, 2, 4] => select 4 => [0, 1, 2, 4]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 3, newSelectedWallet = userWallet3),
|
|
||||||
// [0, 1, 2, 3, *4*] => delete 4 => [0, 1, 2, 3] => select 3 => [0, 1, 2, *3*]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 4, newSelectedWallet = userWallet3),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getTestsIfPrevSelectedIndexIs0() = listOf(
|
|
||||||
// [*0*, -1-, 2, 3, 4] => delete 0 => [-1-, 2, 3, 4] => select 2 => [-1-, *2*, 3, 4]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(lockedUserWallet0, userWallet1, userWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 0,
|
|
||||||
newSelectedWallet = userWallet1,
|
|
||||||
),
|
|
||||||
// [*0*, -1-, -2-, 3, 4] => delete 0 => [-1-, -2-, 3, 4] => select 3 => [-1-, -2-, *3*, 4]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, userWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 0,
|
|
||||||
newSelectedWallet = userWallet2,
|
|
||||||
),
|
|
||||||
// [*0*, -1-, -2-, -3-, 4] => delete 0 => [-1-, -2-, -3-, 4] => select 4 => [-1-, -2-, -3-, *4*]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 0,
|
|
||||||
newSelectedWallet = userWallet3,
|
|
||||||
),
|
|
||||||
// [*0*, -1-, -2-, -3-, -4-] => delete 0 => [-1-, -2-, -3-, -4-] => select 1 => [*-1-*, -2-, -3-, -4-]
|
|
||||||
Model(userWallets = lockedWallets, prevSelectedIndex = 0, newSelectedWallet = lockedUserWallet0),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getTestsIfPrevSelectedIndexIsLastIndex() = listOf(
|
|
||||||
// [0, 1, 2, -3-, *4*] => delete 4 => [0, 1, 2, -3-] => select 2 => [0, 1, *2*, -3-]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, userWallet1, userWallet2, lockedUserWallet3),
|
|
||||||
prevSelectedIndex = 4,
|
|
||||||
newSelectedWallet = userWallet2,
|
|
||||||
),
|
|
||||||
// [0, 1, -2-, -3-, *4*] => delete 4 => [0, 1, -2-, -3-] => select 1 => [0, *1*, -2-, -3-]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, userWallet1, lockedUserWallet2, lockedUserWallet3),
|
|
||||||
prevSelectedIndex = 4,
|
|
||||||
newSelectedWallet = userWallet1,
|
|
||||||
),
|
|
||||||
// [0, -1-, -2-, -3-, *4*] => delete 4 => [0, -1-, -2-, -3-] => select 0 => [*0*, -1-, -2-, -3-]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3),
|
|
||||||
prevSelectedIndex = 4,
|
|
||||||
newSelectedWallet = userWallet0,
|
|
||||||
),
|
|
||||||
// [-0-, -1-, -2-, -3-, *4*] => delete 4 => [-0-, -1-, -2-, -3-] => select 3 => [-0-, -1-, -2-, *-3-*]
|
|
||||||
Model(userWallets = lockedWallets, prevSelectedIndex = 4, newSelectedWallet = lockedUserWallet3),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getTestsIfNewSelectedIndexIsNearby() = listOf(
|
|
||||||
// [0, *1*, 2, 3, 4] => delete 1 => [0, 2, 3, 4] => select 2 => [0, *2*, 3, 4]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 1, newSelectedWallet = userWallet1),
|
|
||||||
// [0, *1*, -2-, 3, 4] => delete 1 => [0, -2-, 3, 4] => select 3 => [0, -2-, *3*, 4]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, lockedUserWallet1, userWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 1,
|
|
||||||
newSelectedWallet = userWallet2,
|
|
||||||
),
|
|
||||||
// [0, *1*, -2-, -3-, 4] => delete 1 => [0, -2-, -3-, 4] => select 0 => [*0*, -2-, -3-, 4]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 1,
|
|
||||||
newSelectedWallet = userWallet0,
|
|
||||||
),
|
|
||||||
// [0, 1, 2, *3*, 4] => delete 3 => [0, 1, 2, 4] => select 4 => [0, 1, 2, *4*]
|
|
||||||
Model(userWallets = unlockedWallets, prevSelectedIndex = 3, newSelectedWallet = userWallet3),
|
|
||||||
// [0, 1, 2, *3*, -4-] => delete 3 => [0, 1, 2, -4-] => select 2 => [0, 1, *2*, -4-]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, userWallet1, userWallet2, lockedUserWallet3),
|
|
||||||
prevSelectedIndex = 3,
|
|
||||||
newSelectedWallet = userWallet2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getTestsIfNewSelectedIndexIsThroughOne() = listOf(
|
|
||||||
// [-0-, *1*, -2-, 3, 4] => delete 1 => [-0-, -2-, 3, 4] => select 3 => [-0-, -2-, *3*, 4]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, userWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 1,
|
|
||||||
newSelectedWallet = userWallet2,
|
|
||||||
),
|
|
||||||
// [-0-, *1*, -2-, -3-, 4] => delete 1 => [-0-, -2-, -3-, 4] => select 4 => [-0-, -2-, -3-, *4*]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3),
|
|
||||||
prevSelectedIndex = 1,
|
|
||||||
newSelectedWallet = userWallet3,
|
|
||||||
),
|
|
||||||
// [0, 1, -2-, *3*, -4-] => delete 3 => [0, 1, -2-, -4-] => select 1 => [0, *1*, -2-, -4-]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, userWallet1, lockedUserWallet2, lockedUserWallet3),
|
|
||||||
prevSelectedIndex = 3,
|
|
||||||
newSelectedWallet = userWallet1,
|
|
||||||
),
|
|
||||||
// [0, -1-, -2-, *3*, -4-] => delete 3 => [*0*, -1-, -2-, -4-] => select 0 => [0, -1-, -2-, -4-]
|
|
||||||
Model(
|
|
||||||
userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3),
|
|
||||||
prevSelectedIndex = 3,
|
|
||||||
newSelectedWallet = userWallet0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun createUserWallet(id: String, isLocked: Boolean): UserWallet {
|
|
||||||
return UserWallet.Cold(
|
|
||||||
name = "Wallet $id",
|
|
||||||
walletId = UserWalletId(stringValue = id),
|
|
||||||
cardsInWallet = emptySet(),
|
|
||||||
isMultiCurrency = true,
|
|
||||||
hasBackupError = false,
|
|
||||||
scanResponse = MockScanResponseFactory.create(
|
|
||||||
cardConfig = GenericCardConfig(maxWalletCount = 1),
|
|
||||||
derivedKeys = emptyMap(),
|
|
||||||
).let {
|
|
||||||
if (isLocked) {
|
|
||||||
it.copy(
|
|
||||||
card = it.card.copy(wallets = emptyList()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,6 +13,10 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(projects.core.utils)
|
implementation(projects.core.utils)
|
||||||
|
api(projects.domain.models)
|
||||||
|
api(projects.domain.appCurrency.models)
|
||||||
|
api(projects.domain.staking.models)
|
||||||
|
api(projects.libs.crypto)
|
||||||
|
|
||||||
// region Firebase libraries
|
// region Firebase libraries
|
||||||
implementation(platform(deps.firebase.bom))
|
implementation(platform(deps.firebase.bom))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.tangem.common
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
|
import com.tangem.utils.extensions.orZero
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the total fiat amount by adding the main fiat amount and the fiat value of the staked balance.
|
||||||
|
*/
|
||||||
|
fun CryptoCurrencyStatus.getTotalFiatAmount(): BigDecimal? {
|
||||||
|
val fiatAmount = value.fiatAmount
|
||||||
|
|
||||||
|
val fiatStakedBalance = value.fiatRate?.times(getStakedBalance().orZero()) ?: return fiatAmount
|
||||||
|
val totalAmount = fiatAmount?.plus(fiatStakedBalance) ?: return fiatStakedBalance
|
||||||
|
|
||||||
|
return totalAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the total cryptocurrency amount by adding the main crypto amount and the staked balance.
|
||||||
|
*/
|
||||||
|
fun CryptoCurrencyStatus.getTotalCryptoAmount(): BigDecimal? {
|
||||||
|
val cryptoAmount = value.amount
|
||||||
|
|
||||||
|
val cryptoStakedBalance = getStakedBalance() ?: return cryptoAmount
|
||||||
|
val totalAmount = cryptoAmount?.plus(cryptoStakedBalance) ?: return cryptoStakedBalance
|
||||||
|
|
||||||
|
return totalAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data)
|
||||||
|
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.domain.staking.utils
|
package com.tangem.common
|
||||||
|
|
||||||
import com.tangem.domain.models.staking.BalanceType
|
import com.tangem.domain.models.staking.BalanceType
|
||||||
import com.tangem.domain.models.staking.StakingBalance
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
|
|
@ -9,6 +9,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api(projects.common)
|
||||||
|
|
||||||
/** Compose */
|
/** Compose */
|
||||||
implementation(deps.compose.material3)
|
implementation(deps.compose.material3)
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ private fun SwapStoriesText(current: SwapStoriesUM.Content.Config) {
|
||||||
),
|
),
|
||||||
color = TangemTheme.colors.text.constantWhite,
|
color = TangemTheme.colors.text.constantWhite,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.testTag(SwapStoriesScreenTestTags.TITLE),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = current.subtitle.resolveReference(),
|
text = current.subtitle.resolveReference(),
|
||||||
|
|
@ -105,6 +106,7 @@ private fun SwapStoriesText(current: SwapStoriesUM.Content.Config) {
|
||||||
),
|
),
|
||||||
color = SubtitleColor,
|
color = SubtitleColor,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.testTag(SwapStoriesScreenTestTags.SUBTITLE),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.tangem.common.ui.tokens
|
package com.tangem.common.ui.tokens
|
||||||
|
|
||||||
|
import com.tangem.common.getTotalCryptoAmount
|
||||||
|
import com.tangem.common.getTotalFiatAmount
|
||||||
import com.tangem.common.ui.R
|
import com.tangem.common.ui.R
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||||
|
|
@ -25,11 +27,8 @@ import com.tangem.domain.staking.model.StakingAvailability
|
||||||
import com.tangem.domain.staking.model.StakingOption
|
import com.tangem.domain.staking.model.StakingOption
|
||||||
import com.tangem.domain.staking.model.common.RewardInfo
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
import com.tangem.domain.staking.model.common.RewardType
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
|
||||||
import com.tangem.lib.crypto.BlockchainUtils
|
import com.tangem.lib.crypto.BlockchainUtils
|
||||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import com.tangem.utils.extensions.orZero
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
|
@ -156,29 +155,6 @@ class TokenItemStateConverter(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun CryptoCurrencyStatus.getFormattedFiatAmount(appCurrency: AppCurrency): String {
|
|
||||||
val fiatAmount = value.fiatAmount ?: return DASH_SIGN
|
|
||||||
|
|
||||||
val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero()
|
|
||||||
val totalAmount = fiatAmount.plus(fiatYieldBalance)
|
|
||||||
|
|
||||||
return totalAmount.format {
|
|
||||||
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CryptoCurrencyStatus.getFormattedCryptoAmount(): String {
|
|
||||||
val cryptoAmount = value.amount ?: return DASH_SIGN
|
|
||||||
|
|
||||||
val totalAmount = cryptoAmount.plus(getStakedBalance())
|
|
||||||
|
|
||||||
return totalAmount.format { crypto(currency) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data)
|
|
||||||
?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero()
|
|
||||||
|
|
||||||
private fun createTitleState(
|
private fun createTitleState(
|
||||||
currencyStatus: CryptoCurrencyStatus,
|
currencyStatus: CryptoCurrencyStatus,
|
||||||
yieldModuleApyMap: Map<String, BigDecimal>,
|
yieldModuleApyMap: Map<String, BigDecimal>,
|
||||||
|
|
@ -348,7 +324,9 @@ class TokenItemStateConverter(
|
||||||
is CryptoCurrencyStatus.NoAccount,
|
is CryptoCurrencyStatus.NoAccount,
|
||||||
-> {
|
-> {
|
||||||
TokenItemState.Subtitle2State.TextContent(
|
TokenItemState.Subtitle2State.TextContent(
|
||||||
text = status.getFormattedCryptoAmount(),
|
text = status.getTotalCryptoAmount().format {
|
||||||
|
crypto(status.currency)
|
||||||
|
},
|
||||||
isFlickering = status.value.isFlickering(),
|
isFlickering = status.value.isFlickering(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +349,12 @@ class TokenItemStateConverter(
|
||||||
is CryptoCurrencyStatus.NoAccount,
|
is CryptoCurrencyStatus.NoAccount,
|
||||||
-> {
|
-> {
|
||||||
TokenItemState.FiatAmountState.Content(
|
TokenItemState.FiatAmountState.Content(
|
||||||
text = status.getFormattedFiatAmount(appCurrency = appCurrency),
|
text = status.getTotalFiatAmount().format {
|
||||||
|
fiat(
|
||||||
|
fiatCurrencyCode = appCurrency.code,
|
||||||
|
fiatCurrencySymbol = appCurrency.symbol,
|
||||||
|
)
|
||||||
|
},
|
||||||
isFlickering = status.value.isFlickering(),
|
isFlickering = status.value.isFlickering(),
|
||||||
icons = buildList {
|
icons = buildList {
|
||||||
if (status.value.yieldSupplyStatus?.isActive == true &&
|
if (status.value.yieldSupplyStatus?.isActive == true &&
|
||||||
|
|
|
||||||
|
|
@ -31,18 +31,10 @@
|
||||||
"name": "SWAP_REDESIGN_ENABLED",
|
"name": "SWAP_REDESIGN_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "HOT_WALLET_ENABLED",
|
|
||||||
"version": "5.32.0"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
|
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
|
||||||
"version": "5.32.0"
|
"version": "5.32.0"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "HOT_WALLET_VISIBLE",
|
|
||||||
"version": "5.32.1"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "TANGEM_PAY_ENABLED",
|
"name": "TANGEM_PAY_ENABLED",
|
||||||
"version": "5.31.0"
|
"version": "5.31.0"
|
||||||
|
|
@ -82,5 +74,9 @@
|
||||||
{
|
{
|
||||||
"name": "EARN_BLOCK_ENABLED",
|
"name": "EARN_BLOCK_ENABLED",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WALLET_REORDER_FEATURE_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -205,4 +205,18 @@ interface TangemTechApi {
|
||||||
*/
|
*/
|
||||||
@POST("v1/transaction-events")
|
@POST("v1/transaction-events")
|
||||||
suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse<Unit>
|
suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse<Unit>
|
||||||
|
|
||||||
|
// region Earn
|
||||||
|
@GET("v1/earn/markets")
|
||||||
|
suspend fun getEarnTokens(
|
||||||
|
@Query("isForEarn") isForEarn: Boolean?,
|
||||||
|
@Query("page") page: String? = null,
|
||||||
|
@Query("limit") limit: Int? = null,
|
||||||
|
@Query("type") type: String? = null,
|
||||||
|
@Query("network") network: String? = null,
|
||||||
|
): ApiResponse<EarnListResponse>
|
||||||
|
|
||||||
|
@GET("v1/earn/networks")
|
||||||
|
suspend fun getEarnNetworks(@Query("type") type: String): ApiResponse<EarnNetworkListResponse>
|
||||||
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.datasource.api.tangemTech.models
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EarnNetworkListResponse(
|
||||||
|
@Json(name = "items") val items: List<EarnNetworkResponse>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EarnNetworkResponse(
|
||||||
|
@Json(name = "networkId") val networkId: String,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.tangem.datasource.api.tangemTech.models
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EarnListResponse(
|
||||||
|
@Json(name = "meta") val meta: MetaEarnListResponse,
|
||||||
|
@Json(name = "items") val items: List<EarnResponse>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EarnResponse(
|
||||||
|
@Json(name = "apy") val apy: String,
|
||||||
|
@Json(name = "networkId") val networkId: String,
|
||||||
|
@Json(name = "rewardType") val rewardType: String,
|
||||||
|
@Json(name = "type") val type: String,
|
||||||
|
@Json(name = "token") val token: EarnTokenResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class EarnTokenResponse(
|
||||||
|
@Json(name = "id") val id: String,
|
||||||
|
@Json(name = "symbol") val symbol: String,
|
||||||
|
@Json(name = "name") val name: String,
|
||||||
|
@Json(name = "address") val address: String,
|
||||||
|
@Json(name = "decimalCount") val decimalCount: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class MetaEarnListResponse(
|
||||||
|
@Json(name = "page") val page: Int,
|
||||||
|
@Json(name = "limit") val limit: Int,
|
||||||
|
)
|
||||||
|
|
@ -30,6 +30,10 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
||||||
apiKey = value.quiknodePlasmaApiKey,
|
apiKey = value.quiknodePlasmaApiKey,
|
||||||
subdomain = value.quiknodePlasmaSubdomain,
|
subdomain = value.quiknodePlasmaSubdomain,
|
||||||
),
|
),
|
||||||
|
quickNodeMonadCredentials = QuickNodeCredentials(
|
||||||
|
apiKey = value.quiknodeMonadApiKey,
|
||||||
|
subdomain = value.quiknodeMonadSubdomain,
|
||||||
|
),
|
||||||
infuraProjectId = value.infuraProjectId,
|
infuraProjectId = value.infuraProjectId,
|
||||||
tronGridApiKey = value.tronGridApiKey,
|
tronGridApiKey = value.tronGridApiKey,
|
||||||
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
||||||
|
|
@ -109,6 +113,8 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
||||||
sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC),
|
sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC),
|
||||||
telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC),
|
telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC),
|
||||||
tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest),
|
tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest),
|
||||||
|
monad = GetBlockAccessToken(rest = accessTokens.monad?.rest),
|
||||||
|
stellar = GetBlockAccessToken(rest = accessTokens.stellar?.rest),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class EnvironmentConfigModel(
|
||||||
@Json(name = "bscQuiknodeApiKey") val bscQuiknodeApiKey: String,
|
@Json(name = "bscQuiknodeApiKey") val bscQuiknodeApiKey: String,
|
||||||
@Json(name = "quiknodePlasmaSubdomain") val quiknodePlasmaSubdomain: String,
|
@Json(name = "quiknodePlasmaSubdomain") val quiknodePlasmaSubdomain: String,
|
||||||
@Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String,
|
@Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String,
|
||||||
|
@Json(name = "quiknodeMonadSubdomain") val quiknodeMonadSubdomain: String,
|
||||||
|
@Json(name = "quiknodeMonadApiKey") val quiknodeMonadApiKey: String,
|
||||||
@Json(name = "nowNodesApiKey") val nowNodesApiKey: String,
|
@Json(name = "nowNodesApiKey") val nowNodesApiKey: String,
|
||||||
@Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?,
|
@Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?,
|
||||||
@Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys,
|
@Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys,
|
||||||
|
|
@ -96,6 +98,8 @@ data class GetBlockAccessTokens(
|
||||||
@Json(name = "sui") val sui: GetBlockToken?,
|
@Json(name = "sui") val sui: GetBlockToken?,
|
||||||
@Json(name = "telos") val telos: GetBlockToken?,
|
@Json(name = "telos") val telos: GetBlockToken?,
|
||||||
@Json(name = "tezos") val tezos: GetBlockToken?,
|
@Json(name = "tezos") val tezos: GetBlockToken?,
|
||||||
|
@Json(name = "monad") val monad: GetBlockToken?,
|
||||||
|
@Json(name = "stellar") val stellar: GetBlockToken?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.tangem.core.navigation.review
|
||||||
|
|
||||||
|
class DummyReviewManager : ReviewManager {
|
||||||
|
override fun request(onDismissClick: () -> Unit) = Unit
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.tangem.core.navigation.review
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to manage in-app review requests.
|
||||||
|
*/
|
||||||
|
interface ReviewManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests an in-app review flow.
|
||||||
|
*/
|
||||||
|
fun request(onDismissClick: () -> Unit)
|
||||||
|
}
|
||||||
|
|
@ -468,9 +468,12 @@
|
||||||
<string name="domain_receive_assets_onboarding_network_name">%s Netzwerk</string>
|
<string name="domain_receive_assets_onboarding_network_name">%s Netzwerk</string>
|
||||||
<string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string>
|
<string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string>
|
||||||
<string name="earn_best_opportunities">Beste Gelegenheiten</string>
|
<string name="earn_best_opportunities">Beste Gelegenheiten</string>
|
||||||
|
<string name="earn_clear_filter">Filter löschen</string>
|
||||||
<string name="earn_filter_all_networks">Alle Netzwerke</string>
|
<string name="earn_filter_all_networks">Alle Netzwerke</string>
|
||||||
<string name="earn_filter_all_types">Alle Arten</string>
|
<string name="earn_filter_all_types">Alle Arten</string>
|
||||||
|
<string name="earn_filter_by">Filtern nach</string>
|
||||||
<string name="earn_mostly_used">Meist verwendet</string>
|
<string name="earn_mostly_used">Meist verwendet</string>
|
||||||
|
<string name="earn_no_results">Keine Ergebnisse</string>
|
||||||
<string name="earn_title">Verdienen</string>
|
<string name="earn_title">Verdienen</string>
|
||||||
<string name="email_preface_wc_error">Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt.</string>
|
<string name="email_preface_wc_error">Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt.</string>
|
||||||
<string name="email_subject_wc_error">WalletConnect-Fehler</string>
|
<string name="email_subject_wc_error">WalletConnect-Fehler</string>
|
||||||
|
|
|
||||||
|
|
@ -468,9 +468,14 @@
|
||||||
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
|
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
|
||||||
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
|
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
|
||||||
<string name="earn_best_opportunities">Meilleures opportunités</string>
|
<string name="earn_best_opportunities">Meilleures opportunités</string>
|
||||||
|
<string name="earn_clear_filter">Effacer le filtre</string>
|
||||||
<string name="earn_filter_all_networks">Tous les réseaux</string>
|
<string name="earn_filter_all_networks">Tous les réseaux</string>
|
||||||
<string name="earn_filter_all_types">Tous les types</string>
|
<string name="earn_filter_all_types">Tous les types</string>
|
||||||
|
<string name="earn_filter_by">Filtrer par</string>
|
||||||
|
<string name="earn_filter_my_networks">Mes réseaux</string>
|
||||||
|
<string name="earn_filter_networks">Réseaux</string>
|
||||||
<string name="earn_mostly_used">Principalement utilisé</string>
|
<string name="earn_mostly_used">Principalement utilisé</string>
|
||||||
|
<string name="earn_no_results">Pas de résultat</string>
|
||||||
<string name="earn_title">Gagner</string>
|
<string name="earn_title">Gagner</string>
|
||||||
<string name="email_preface_wc_error">Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s</string>
|
<string name="email_preface_wc_error">Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s</string>
|
||||||
<string name="email_subject_wc_error">Erreur WalletConnect</string>
|
<string name="email_subject_wc_error">Erreur WalletConnect</string>
|
||||||
|
|
@ -823,6 +828,7 @@
|
||||||
<string name="markets_token_details_volume">Volume</string>
|
<string name="markets_token_details_volume">Volume</string>
|
||||||
<string name="markets_tooltip_message">Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché</string>
|
<string name="markets_tooltip_message">Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché</string>
|
||||||
<string name="markets_tooltip_title">Ajouter des jetons</string>
|
<string name="markets_tooltip_title">Ajouter des jetons</string>
|
||||||
|
<string name="markets_tooltip_v2_title">Ajouter plus de tokens</string>
|
||||||
<string name="markets_yield_supply_banner_description">Optimisez vos actifs tout en leur fournissant un accès instantané. %s</string>
|
<string name="markets_yield_supply_banner_description">Optimisez vos actifs tout en leur fournissant un accès instantané. %s</string>
|
||||||
<string name="markets_yield_supply_banner_title">Activer le mode rendement</string>
|
<string name="markets_yield_supply_banner_title">Activer le mode rendement</string>
|
||||||
<string name="mobile_wallet_requires_min_os_warning_body">Vous devez effectuer la mise à jour %1$s afin de créer un portefeuille mobile.</string>
|
<string name="mobile_wallet_requires_min_os_warning_body">Vous devez effectuer la mise à jour %1$s afin de créer un portefeuille mobile.</string>
|
||||||
|
|
@ -1849,6 +1855,7 @@
|
||||||
<string name="warning_button_refresh">Rafraîchir</string>
|
<string name="warning_button_refresh">Rafraîchir</string>
|
||||||
<string name="warning_clore_migration_button">Lancer la migration</string>
|
<string name="warning_clore_migration_button">Lancer la migration</string>
|
||||||
<string name="warning_clore_migration_copy_button">Copier</string>
|
<string name="warning_clore_migration_copy_button">Copier</string>
|
||||||
|
<string name="warning_clore_migration_description">Pour conserver l\'accès à vos fonds, lancez la migration conformément aux directives officielles de Clore.</string>
|
||||||
<string name="warning_clore_migration_error_signing_not_supported">La signature des messages n\'est pas prise en charge pour ce réseau.</string>
|
<string name="warning_clore_migration_error_signing_not_supported">La signature des messages n\'est pas prise en charge pour ce réseau.</string>
|
||||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Impossible de signer le message. Veuillez réessayer.</string>
|
<string name="warning_clore_migration_error_wallet_manager_not_found">Impossible de signer le message. Veuillez réessayer.</string>
|
||||||
<string name="warning_clore_migration_message">Selon la documentation officielle de Clore, toutes les pièces reçues avant le 21 décembre seront migrées vers Clore (token ERC-20) ; les pièces reçues après cette date ne le seront pas. Une solution de transfert arrive — restez à l\'écoute.</string>
|
<string name="warning_clore_migration_message">Selon la documentation officielle de Clore, toutes les pièces reçues avant le 21 décembre seront migrées vers Clore (token ERC-20) ; les pièces reçues après cette date ne le seront pas. Une solution de transfert arrive — restez à l\'écoute.</string>
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,7 @@
|
||||||
<string name="common_go_to_token">Перейти в токен</string>
|
<string name="common_go_to_token">Перейти в токен</string>
|
||||||
<string name="common_got_it">Понятно</string>
|
<string name="common_got_it">Понятно</string>
|
||||||
<string name="common_hide">Скрыть</string>
|
<string name="common_hide">Скрыть</string>
|
||||||
|
<string name="common_hold_to">Удерживайте, чтобы %s</string>
|
||||||
<string name="common_hour">час</string>
|
<string name="common_hour">час</string>
|
||||||
<string name="common_import">Импортировать</string>
|
<string name="common_import">Импортировать</string>
|
||||||
<string name="common_in_progress">В процессе</string>
|
<string name="common_in_progress">В процессе</string>
|
||||||
|
|
@ -387,6 +388,7 @@
|
||||||
<string name="common_swap">Обменять</string>
|
<string name="common_swap">Обменять</string>
|
||||||
<string name="common_tangem">Tangem</string>
|
<string name="common_tangem">Tangem</string>
|
||||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||||
|
<string name="common_tap_and_hold_hint">Нажмите и удерживайте</string>
|
||||||
<string name="common_terms_and_conditions">условия участия</string>
|
<string name="common_terms_and_conditions">условия участия</string>
|
||||||
<string name="common_terms_of_use">Условиями использования</string>
|
<string name="common_terms_of_use">Условиями использования</string>
|
||||||
<string name="common_to">На</string>
|
<string name="common_to">На</string>
|
||||||
|
|
@ -478,9 +480,14 @@
|
||||||
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
|
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
|
||||||
<string name="domain_receive_assets_onboarding_network_name">%s сеть</string>
|
<string name="domain_receive_assets_onboarding_network_name">%s сеть</string>
|
||||||
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>
|
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>
|
||||||
|
<string name="earn_best_opportunities">Лучшие возможности</string>
|
||||||
|
<string name="earn_clear_filter">Очистить фильтр</string>
|
||||||
<string name="earn_filter_all_networks">Все сети</string>
|
<string name="earn_filter_all_networks">Все сети</string>
|
||||||
<string name="earn_filter_all_types">Все типы</string>
|
<string name="earn_filter_all_types">Все типы</string>
|
||||||
|
<string name="earn_filter_by">Фильтровать по</string>
|
||||||
|
<string name="earn_filter_networks">Сети</string>
|
||||||
<string name="earn_mostly_used">Часто используемые</string>
|
<string name="earn_mostly_used">Часто используемые</string>
|
||||||
|
<string name="earn_no_results">Нет результата</string>
|
||||||
<string name="email_preface_wc_error">Привет, команда поддержки, у меня возникла ошибка с кодом: %s</string>
|
<string name="email_preface_wc_error">Привет, команда поддержки, у меня возникла ошибка с кодом: %s</string>
|
||||||
<string name="email_subject_wc_error">Ошибка WalletConnect</string>
|
<string name="email_subject_wc_error">Ошибка WalletConnect</string>
|
||||||
<string name="error_wrong_wallet_tapped">Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком.</string>
|
<string name="error_wrong_wallet_tapped">Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком.</string>
|
||||||
|
|
@ -1810,6 +1817,7 @@
|
||||||
<string name="warning_button_refresh">Обновить</string>
|
<string name="warning_button_refresh">Обновить</string>
|
||||||
<string name="warning_clore_migration_button">Начать миграцию</string>
|
<string name="warning_clore_migration_button">Начать миграцию</string>
|
||||||
<string name="warning_clore_migration_copy_button">Копировать</string>
|
<string name="warning_clore_migration_copy_button">Копировать</string>
|
||||||
|
<string name="warning_clore_migration_description">Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальными рекомендациями Clore.</string>
|
||||||
<string name="warning_clore_migration_error_signing_not_supported">Подписание сообщений не поддерживается в этой сети</string>
|
<string name="warning_clore_migration_error_signing_not_supported">Подписание сообщений не поддерживается в этой сети</string>
|
||||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Невозможно подписать сообщение. Пожалуйста, попробуй позже.</string>
|
<string name="warning_clore_migration_error_wallet_manager_not_found">Невозможно подписать сообщение. Пожалуйста, попробуй позже.</string>
|
||||||
<string name="warning_clore_migration_message">Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями.</string>
|
<string name="warning_clore_migration_message">Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями.</string>
|
||||||
|
|
|
||||||
|
|
@ -469,9 +469,11 @@
|
||||||
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
|
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
|
||||||
<string name="earn_best_opportunities">Best opportunities</string>
|
<string name="earn_best_opportunities">Best opportunities</string>
|
||||||
<string name="earn_clear_filter">Clear filter</string>
|
<string name="earn_clear_filter">Clear filter</string>
|
||||||
|
<string name="earn_empty">The list is temporarily empty as it’s being refreshed. Check back in a moment.</string>
|
||||||
<string name="earn_filter_all_networks">All networks</string>
|
<string name="earn_filter_all_networks">All networks</string>
|
||||||
<string name="earn_filter_all_types">All types</string>
|
<string name="earn_filter_all_types">All types</string>
|
||||||
<string name="earn_filter_by">Filter by</string>
|
<string name="earn_filter_by">Filter by</string>
|
||||||
|
<string name="earn_filter_my_networks">My networks</string>
|
||||||
<string name="earn_filter_networks">Networks</string>
|
<string name="earn_filter_networks">Networks</string>
|
||||||
<string name="earn_mostly_used">Mostly used</string>
|
<string name="earn_mostly_used">Mostly used</string>
|
||||||
<string name="earn_no_results">No results</string>
|
<string name="earn_no_results">No results</string>
|
||||||
|
|
@ -828,6 +830,7 @@
|
||||||
<string name="markets_token_details_volume">Volume</string>
|
<string name="markets_token_details_volume">Volume</string>
|
||||||
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
|
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
|
||||||
<string name="markets_tooltip_title">Add tokens</string>
|
<string name="markets_tooltip_title">Add tokens</string>
|
||||||
|
<string name="markets_tooltip_v2_title">Add more tokens</string>
|
||||||
<string name="markets_yield_supply_banner_description">Power up your assets while supplying them with instant access. %s</string>
|
<string name="markets_yield_supply_banner_description">Power up your assets while supplying them with instant access. %s</string>
|
||||||
<string name="markets_yield_supply_banner_title">Activate Yield Mode</string>
|
<string name="markets_yield_supply_banner_title">Activate Yield Mode</string>
|
||||||
<string name="mobile_wallet_requires_min_os_warning_body">You must update to %1$s before creating a mobile wallet</string>
|
<string name="mobile_wallet_requires_min_os_warning_body">You must update to %1$s before creating a mobile wallet</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.core.ui.components.buttons.actions
|
||||||
|
|
||||||
|
import androidx.compose.ui.semantics.SemanticsPropertyKey
|
||||||
|
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
|
||||||
|
|
||||||
|
val IsDimmedKey = SemanticsPropertyKey<Boolean>("IsDimmed")
|
||||||
|
val HasBadgeKey = SemanticsPropertyKey<Boolean>("HasBadge")
|
||||||
|
|
||||||
|
var SemanticsPropertyReceiver.isDimmed by IsDimmedKey
|
||||||
|
var SemanticsPropertyReceiver.hasBadge by HasBadgeKey
|
||||||
|
|
@ -22,7 +22,6 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -136,11 +135,8 @@ fun ActionBaseButton(
|
||||||
}
|
}
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.semantics {
|
.semantics {
|
||||||
contentDescription = if (config.shouldDimContent) {
|
isDimmed = config.shouldDimContent
|
||||||
"Action button is dimmed"
|
hasBadge = config.shouldShowBadge
|
||||||
} else {
|
|
||||||
"Action button is not dimmed"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
enabled = config.isEnabled,
|
enabled = config.isEnabled,
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,14 @@ private const val DEFAULT_SCALE = 1f
|
||||||
|
|
||||||
private const val SHAKE_AMPLITUDE_DP = 5f
|
private const val SHAKE_AMPLITUDE_DP = 5f
|
||||||
private const val SHAKE_DURATION_MS = 120
|
private const val SHAKE_DURATION_MS = 120
|
||||||
private const val FADE_DURATION_MS = 150
|
private const val FADE_DURATION_MS = 100
|
||||||
|
|
||||||
private const val BOUNCE_VELOCITY_MULTIPLIER = 1.25f
|
private const val BOUNCE_VELOCITY_MULTIPLIER = 1.25f
|
||||||
|
|
||||||
private const val HAPTIC_INITIAL_INTERVAL_MS = 200L
|
private const val HAPTIC_INITIAL_INTERVAL_MS = 50L
|
||||||
private const val HAPTIC_MIN_INTERVAL_MS = 30L
|
private const val HAPTIC_MIN_INTERVAL_MS = 15L
|
||||||
private const val HAPTIC_HEARTBEAT_INTERVAL_MS = 100L
|
private const val HAPTIC_HEARTBEAT_INTERVAL_MS = 100L
|
||||||
|
private const val HAPTIC_SUCCESS_DELAY_MS = 300L
|
||||||
|
|
||||||
// Easing: smooth acceleration, gradually picking up speed
|
// Easing: smooth acceleration, gradually picking up speed
|
||||||
private val AccelerateEasing = CubicBezierEasing(
|
private val AccelerateEasing = CubicBezierEasing(
|
||||||
|
|
@ -288,6 +289,7 @@ private fun Modifier.holdToConfirmGestures(
|
||||||
state.isProgressVisible = true
|
state.isProgressVisible = true
|
||||||
|
|
||||||
// Soft heartbeat on success
|
// Soft heartbeat on success
|
||||||
|
delay(HAPTIC_SUCCESS_DELAY_MS)
|
||||||
performSuccessHapticFeedback(config.hapticManager)
|
performSuccessHapticFeedback(config.hapticManager)
|
||||||
|
|
||||||
state.scaleProgress.animateTo(
|
state.scaleProgress.animateTo(
|
||||||
|
|
@ -324,7 +326,7 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag
|
||||||
// Exponential decrease: interval decreases faster as progress increases
|
// Exponential decrease: interval decreases faster as progress increases
|
||||||
val interval = (maxInterval * (1f - progress * progress) + minInterval).toLong()
|
val interval = (maxInterval * (1f - progress * progress) + minInterval).toLong()
|
||||||
|
|
||||||
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
|
hapticManager.perform(TangemHapticEffect.View.ClockTick)
|
||||||
delay(interval)
|
delay(interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -333,18 +335,18 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag
|
||||||
* Performs strong heartbeat haptic feedback on release (two heavy clicks).
|
* Performs strong heartbeat haptic feedback on release (two heavy clicks).
|
||||||
*/
|
*/
|
||||||
private suspend fun performReleaseHapticFeedback(hapticManager: HapticManager) {
|
private suspend fun performReleaseHapticFeedback(hapticManager: HapticManager) {
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick)
|
hapticManager.perform(TangemHapticEffect.View.Reject)
|
||||||
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick)
|
hapticManager.perform(TangemHapticEffect.View.Reject)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs soft heartbeat haptic feedback on success (two light clicks).
|
* Performs soft heartbeat haptic feedback on success (two light clicks).
|
||||||
*/
|
*/
|
||||||
private suspend fun performSuccessHapticFeedback(hapticManager: HapticManager) {
|
private suspend fun performSuccessHapticFeedback(hapticManager: HapticManager) {
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.Click)
|
hapticManager.perform(TangemHapticEffect.View.Confirm)
|
||||||
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
delay(HAPTIC_HEARTBEAT_INTERVAL_MS)
|
||||||
hapticManager.perform(TangemHapticEffect.OneTime.Click)
|
hapticManager.perform(TangemHapticEffect.View.Confirm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun CoroutineScope.handleReleaseAnimation(
|
private suspend fun CoroutineScope.handleReleaseAnimation(
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||||
|
import com.tangem.core.ui.test.SearchBarTestTags
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(
|
||||||
|
|
@ -126,7 +127,9 @@ private fun DecorationBox(
|
||||||
contentPadding = PaddingValues(all = TangemTheme.dimens.spacing12),
|
contentPadding = PaddingValues(all = TangemTheme.dimens.spacing12),
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(TangemTheme.dimens.size20),
|
modifier = Modifier
|
||||||
|
.size(TangemTheme.dimens.size20)
|
||||||
|
.testTag(SearchBarTestTags.ICON),
|
||||||
painter = painterResource(id = R.drawable.ic_search_24),
|
painter = painterResource(id = R.drawable.ic_search_24),
|
||||||
tint = TangemTheme.colors.icon.informative,
|
tint = TangemTheme.colors.icon.informative,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
@ -137,6 +140,7 @@ private fun DecorationBox(
|
||||||
state = state,
|
state = state,
|
||||||
focusManager = focusManager,
|
focusManager = focusManager,
|
||||||
keyboardController = keyboardController,
|
keyboardController = keyboardController,
|
||||||
|
modifier = Modifier.testTag(SearchBarTestTags.CLEAR_BUTTON),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
placeholder = {
|
placeholder = {
|
||||||
|
|
@ -146,6 +150,7 @@ private fun DecorationBox(
|
||||||
style = TangemTheme.typography.body2,
|
style = TangemTheme.typography.body2,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.testTag(SearchBarTestTags.PLACEHOLDER_TEXT),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,20 @@ import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.components.SpacerW4
|
import com.tangem.core.ui.components.SpacerW4
|
||||||
import com.tangem.core.ui.res.TangemColorPalette
|
import com.tangem.core.ui.res.TangemColorPalette
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.test.SwapStoriesScreenTestTags
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
|
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
|
||||||
|
|
@ -99,6 +103,7 @@ fun StoriesProgressBar(
|
||||||
.clip(RoundedCornerShape(2.dp))
|
.clip(RoundedCornerShape(2.dp))
|
||||||
.background(TangemColorPalette.White)
|
.background(TangemColorPalette.White)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
|
.testTag(SwapStoriesScreenTestTags.PROGRESS_BAR_ITEM)
|
||||||
.let { modifier ->
|
.let { modifier ->
|
||||||
when (index) {
|
when (index) {
|
||||||
currentStep -> modifier.fillMaxWidth(progress.value)
|
currentStep -> modifier.fillMaxWidth(progress.value)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue