Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-27 15:59:43 +03:00
commit 71748eff67
517 changed files with 14212 additions and 3419 deletions

View file

@ -18,6 +18,7 @@ android {
namespace = "com.tangem.wallet" namespace = "com.tangem.wallet"
testOptions { testOptions {
animationsDisabled = true animationsDisabled = true
execution = "ANDROIDX_TEST_ORCHESTRATOR"
} }
packaging { packaging {
jniLibs { jniLibs {
@ -71,6 +72,10 @@ configurations.all {
} }
} }
configurations.androidTestImplementation {
exclude(module = "protobuf-lite")
}
dependencies { dependencies {
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
@ -114,6 +119,8 @@ dependencies {
implementation(projects.domain.notifications) implementation(projects.domain.notifications)
implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.models)
implementation(projects.domain.notifications.toggles) implementation(projects.domain.notifications.toggles)
implementation(projects.domain.swap.models)
implementation(projects.domain.swap)
implementation(projects.common) implementation(projects.common)
implementation(projects.common.routing) implementation(projects.common.routing)
@ -160,6 +167,7 @@ dependencies {
implementation(projects.data.quotes) implementation(projects.data.quotes)
implementation(projects.data.blockaid) implementation(projects.data.blockaid)
implementation(projects.data.notifications) implementation(projects.data.notifications)
implementation(projects.data.swap)
/** Features */ /** Features */
implementation(projects.features.referral.impl) implementation(projects.features.referral.impl)
@ -170,6 +178,8 @@ dependencies {
implementation(projects.features.swap.domain) implementation(projects.features.swap.domain)
implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.api)
implementation(projects.features.swap.data) implementation(projects.features.swap.data)
implementation(projects.features.swapV2.api)
implementation(projects.features.swapV2.impl)
implementation(projects.features.tester.api) implementation(projects.features.tester.api)
implementation(projects.features.tester.impl) implementation(projects.features.tester.impl)
implementation(projects.features.wallet.api) implementation(projects.features.wallet.api)
@ -210,8 +220,12 @@ dependencies {
implementation(projects.features.walletconnect.impl) implementation(projects.features.walletconnect.impl)
implementation(projects.features.usedesk.api) implementation(projects.features.usedesk.api)
implementation(projects.features.usedesk.impl) implementation(projects.features.usedesk.impl)
implementation(projects.features.feeSelector.api) implementation(projects.features.hotWallet.api)
implementation(projects.features.feeSelector.impl) implementation(projects.features.hotWallet.impl)
implementation(projects.features.kyc.api)
implementation(projects.features.kyc.impl)
implementation(projects.features.welcome.api)
implementation(projects.features.welcome.impl)
/** AndroidX libraries */ /** AndroidX libraries */
implementation(deps.androidx.core.ktx) implementation(deps.androidx.core.ktx)
@ -310,19 +324,13 @@ dependencies {
testImplementation(deps.test.mockk) testImplementation(deps.test.mockk)
testImplementation(deps.test.truth) testImplementation(deps.test.truth)
androidTestImplementation(deps.test.junit.android) androidTestImplementation(deps.test.junit.android)
androidTestImplementation(deps.test.espresso){ androidTestImplementation(deps.test.espresso)
exclude(group = "com.google.protobuf", module = "protobuf-lite") //conflicting with firebasePerf
}
androidTestImplementation(deps.test.espresso.intents) androidTestImplementation(deps.test.espresso.intents)
{ androidTestImplementation(deps.test.kaspresso)
exclude(group = "com.google.protobuf", module = "protobuf-lite") //conflicting with firebasePerf androidTestImplementation(deps.test.kaspresso.compose)
}
androidTestImplementation(deps.test.compose.junit) androidTestImplementation(deps.test.compose.junit)
androidTestImplementation(deps.test.hamcrest) androidTestImplementation(deps.test.hamcrest)
androidTestImplementation(deps.test.hilt) androidTestImplementation(deps.test.hilt)
androidTestImplementation(deps.test.ultron.android)
androidTestImplementation(deps.test.ultron.compose)
androidTestImplementation(deps.test.ultron.allure)
kaptAndroidTest(deps.test.hilt.compiler) kaptAndroidTest(deps.test.hilt.compiler)
/** Chucker */ /** Chucker */
@ -340,6 +348,8 @@ dependencies {
implementation(deps.listenableFuture) implementation(deps.listenableFuture)
implementation(deps.mlKit.barcodeScanning) implementation(deps.mlKit.barcodeScanning)
androidTestUtil(deps.test.orchestrator)
/** Leakcanary */ /** Leakcanary */
debugImplementation(deps.leakcanary) debugImplementation(deps.leakcanary)

View file

@ -1,66 +1,68 @@
package com.tangem.common package com.tangem.common
import android.Manifest import android.Manifest
import android.content.Context
import android.util.Log
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.datastore.dataStoreFile
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import com.atiurin.ultron.core.compose.config.UltronComposeConfig import com.kaspersky.components.composesupport.config.withComposeSupport
import com.atiurin.ultron.core.compose.createUltronComposeRule import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.atiurin.ultron.core.compose.listeners.ComposDebugListener import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.atiurin.ultron.core.config.UltronCommonConfig
import com.atiurin.ultron.core.config.UltronConfig
import com.atiurin.ultron.core.test.UltronTest
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.MainActivity import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidRule
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import javax.inject.Inject import javax.inject.Inject
abstract class BaseTestCase : UltronTest() { abstract class BaseTestCase : TestCase(
kaspressoBuilder = Kaspresso.Builder.withComposeSupport()
) {
@Inject @Inject
lateinit var tangemSdkManager: TangemSdkManager lateinit var tangemSdkManager: TangemSdkManager
@Inject @Inject
lateinit var appPreferencesStore: AppPreferencesStore lateinit var appPreferencesStore: AppPreferencesStore
@get:Rule(order = 0) private val hiltRule = HiltAndroidRule(this)
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( private val permissionRule = GrantPermissionRule.grant(
Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.CAMERA Manifest.permission.CAMERA,
) )
@get:Rule(order = 1) val composeTestRule = createAndroidComposeRule<MainActivity>()
val hiltRule = HiltAndroidRule(this)
@get:Rule (order = 2) @Rule
val injectionRule = ApplicationInjectionExecutionRule() @JvmField
val ruleChain: TestRule = RuleChain
.outerRule(hiltRule)
.around(ApplicationInjectionExecutionRule())
.around(permissionRule)
.around(composeTestRule)
@get:Rule(order = 3) protected fun setupHooks(
val composeRule = createUltronComposeRule<MainActivity>() additionalBeforeSection: () -> Unit = {},
additionalAfterSection: () -> Unit = {},
override val beforeTest: () -> Unit = { ) = before {
hiltRule.inject() hiltRule.inject()
runBlocking { Intents.init()
delay(INIT_DELAY) additionalBeforeSection()
} }.after {
} additionalAfterSection()
Intents.release()
override val afterTest: () -> Unit = {
runBlocking {
appPreferencesStore.editData { prefs -> prefs.clear() }
}
} }
companion object { companion object {
@BeforeClass private const val INIT_DELAY = 1000L
@JvmStatic
fun config() {
UltronConfig.applyRecommended()
UltronComposeConfig.applyRecommended()
UltronCommonConfig.addListener(ComposDebugListener())
}
private const val INIT_DELAY = 2000L
} }
} }

View file

@ -0,0 +1,8 @@
package com.tangem.common.extensions
import io.github.kakaocup.compose.node.element.KNode
fun KNode.clickWithAssertion() {
assertIsDisplayed()
performClick()
}

View file

@ -1,9 +0,0 @@
package com.tangem.common.extensions
import androidx.test.platform.app.InstrumentationRegistry
object TestDataUtils {
fun getResourceString(resourceId: Int): String {
return InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resourceId)
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.scenarios
import com.atiurin.ultron.allure.step.step
import com.atiurin.ultron.extensions.assertIsDisplayed
import com.atiurin.ultron.extensions.click
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.DisclaimerPage
import com.tangem.screens.MainPage
import com.tangem.screens.StoriesPage
import com.tangem.tap.domain.sdk.mocks.MockProvider
object MainPageScenario {
fun open(productType: ProductType? = null) {
if (productType != null) {
MockProvider.setMocks(productType)
}
step("Click on \"Accept\" button") {
DisclaimerPage.acceptButton.click()
}
step("Click on \"Scan\" button emulating scan error") {
StoriesPage.scanButton.click()
}
step("Assert: main is displayed") {
MainPage.container.assertIsDisplayed()
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.scenarios
import androidx.compose.ui.test.junit4.ComposeTestRule
import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.MainTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.screens.TestTopBar
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.github.kakaocup.compose.node.element.ComposeScreen
class OpenMainScreenScenario(
private val testRule: ComposeTestRule,
private val productType: ProductType? = null,
) : Scenario() {
override val steps: TestContext<Unit>.() -> Unit = {
if (productType != null) {
MockProvider.setMocks(productType)
}
ComposeScreen.onComposeScreen<DisclaimerTestScreen>(testRule) {
step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesTestScreen>(testRule) {
step("Click on \"Scan\" button") {
scanButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<MainTestScreen>(testRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
ComposeScreen.onComposeScreen<TestTopBar>(testRule) {
step("Close Markets tooltip"){
performClick()
}
}
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.hasText
import com.atiurin.ultron.page.Page
import com.tangem.common.extensions.TestDataUtils.getResourceString
import com.tangem.wallet.R
object DetailsPage : Page<DetailsPage>() {
val walletConnectButton = hasText(getResourceString(R.string.wallet_connect_title))
val walletNameButton = hasText(getResourceString(R.string.manage_tokens_network_selector_wallet))
val scanCardButton = hasText(getResourceString(R.string.scan_card_settings_button))
val buyTangemButton = hasText(getResourceString(R.string.details_buy_wallet))
val appSettingsButton = hasText(getResourceString(R.string.app_settings_title))
val contactSupportButton = hasText(getResourceString(R.string.details_row_title_contact_to_support))
val toSButton = hasText(getResourceString(R.string.disclaimer_title))
}

View file

@ -0,0 +1,51 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class DetailsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DetailsTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.DETAILS_SCREEN) }
) {
val walletConnectButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.wallet_connect_title))
}
private val walletBlock: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
}
val walletNameButton: KNode = walletBlock.child {
hasClickAction()
hasPosition(0)
}
val scanCardButton: KNode = walletBlock.child {
hasText(getResourceString(R.string.scan_card_settings_button))
}
val buyTangemButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.details_buy_wallet))
}
val appSettingsButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.app_settings_title))
}
val contactSupportButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.details_row_title_contact_to_support))
}
val toSButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.disclaimer_title))
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.hasTestTag
import com.atiurin.ultron.page.Page
import com.tangem.core.ui.test.TestTags
object DisclaimerPage : Page<DisclaimerPage>() {
val acceptButton = hasTestTag(TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON)
}

View file

@ -0,0 +1,17 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
class DisclaimerTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DisclaimerTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.DISCLAIMER_SCREEN_CONTAINER) }
) {
val acceptButton: KNode = child {
hasTestTag(TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.hasTestTag
import com.atiurin.ultron.page.Page
import com.tangem.core.ui.test.TestTags
object MainPage : Page<MainPage>() {
val container = hasTestTag(TestTags.MAIN_SCREEN)
}

View file

@ -0,0 +1,11 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
class MainTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN) }
)

View file

@ -1,10 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.hasTestTag
import com.atiurin.ultron.page.Page
import com.tangem.core.ui.test.TestTags
object StoriesPage : Page<StoriesPage>() {
val scanButton = hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON)
val orderButton = hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON)
}

View file

@ -0,0 +1,32 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.views.KView
import io.github.kakaocup.kakao.text.KButton
class StoriesTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StoriesTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) }
) {
val scanButton: KNode = child {
hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON)
}
val orderButton: KNode = child {
hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON)
}
val enableNFCAlert: KView = KView {
withId(R.id.alertTitle)
}
val cancelButton: KButton = KButton {
withId(android.R.id.button2)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
class TestTopBar(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TestTopBar>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN_TOP_BAR) }
) {
val moreButton: KNode = child {
hasTestTag(TestTags.MAIN_SCREEN_MORE_BUTTON)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.hasTestTag
import com.atiurin.ultron.page.Page
import com.tangem.core.ui.test.TestTags
object TopBarPage : Page<TopBarPage>() {
val moreButton = hasTestTag(TestTags.MAIN_SCREEN_MORE_BUTTON)
}

View file

@ -1,13 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.hasText
import com.atiurin.ultron.page.Page
import com.tangem.common.extensions.TestDataUtils.getResourceString
import com.tangem.wallet.R
object WalletSettingsPage : Page<WalletSettingsPage>() {
val linkMoreCardsButton = hasText(getResourceString(R.string.details_row_title_create_backup))
val cardSettingsButton = hasText(getResourceString(R.string.card_settings_title))
val referralProgramButton = hasText(getResourceString(R.string.details_referral_title))
val forgetWalletButton = hasText(getResourceString(R.string.settings_forget_wallet))
}

View file

@ -0,0 +1,31 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class WalletSettingsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletSettingsTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.WALLET_SETTINGS_SCREEN) }
) {
private val walletSettingsItem: KNode = child {
hasTestTag(TestTags.WALLET_SETTINGS_SCREEN_ITEM)
}
val linkMoreCardsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_row_title_create_backup))
}
val cardSettingsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.card_settings_title))
}
val referralProgramButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_referral_title))
}
val forgetWalletButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.settings_forget_wallet))
}
}

View file

@ -1,138 +1,157 @@
package com.tangem.tests package com.tangem.tests
import com.atiurin.ultron.allure.step.step
import com.atiurin.ultron.extensions.assertIsDisplayed
import com.atiurin.ultron.extensions.assertIsNotDisplayed
import com.atiurin.ultron.extensions.click
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.MainPageScenario import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.DetailsPage import com.tangem.screens.DetailsTestScreen
import com.tangem.screens.TopBarPage import com.tangem.screens.TestTopBar
import com.tangem.screens.WalletSettingsPage import com.tangem.screens.WalletSettingsTestScreen
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
class DetailsScreenTest : BaseTestCase() { class DetailsScreenTest : BaseTestCase() {
@Test @Test
fun walletWithoutBackupDetails() { fun walletWithoutBackupDetails() =
MainPageScenario.open() setupHooks().run {
step("Open wallet details") { scenario(OpenMainScreenScenario(composeTestRule))
TopBarPage.moreButton.click() ComposeScreen.onComposeScreen<TestTopBar>(composeTestRule) {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<DetailsTestScreen>(composeTestRule) {
step("Assert wallet connect button is visible") {
walletConnectButton.assertIsDisplayed()
}
step("Assert scan card button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
walletNameButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
step("Assert Link more cards button is visible") {
linkMoreCardsButton.assertIsDisplayed()
}
step("Assert Card Settings button is visible") {
cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button is visible") {
referralProgramButton.assertIsDisplayed()
}
step("Assert Forget wallet button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
} }
step("Assert wallet connect button is visible") {
DetailsPage.walletConnectButton.assertIsDisplayed()
}
step("Assert scan card button is visible") {
DetailsPage.scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
DetailsPage.buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
DetailsPage.appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
DetailsPage.contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
DetailsPage.toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
DetailsPage.walletNameButton.click()
}
step("Assert Link more cards button is visible") {
WalletSettingsPage.linkMoreCardsButton.assertIsDisplayed()
}
step("Assert Card Settings button is visible") {
WalletSettingsPage.cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button is visible") {
WalletSettingsPage.referralProgramButton.assertIsDisplayed()
}
step("Assert Forget wallet button is visible") {
WalletSettingsPage.forgetWalletButton.assertIsDisplayed()
}
}
@Test @Test
fun wallet2Details() { fun wallet2Details() =
MainPageScenario.open(ProductType.Wallet2) setupHooks().run {
step("Open wallet details") { scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
TopBarPage.moreButton.click() ComposeScreen.onComposeScreen<TestTopBar>(composeTestRule) {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<DetailsTestScreen>(composeTestRule) {
step("Assert wallet connect button is visible") {
walletConnectButton.assertIsDisplayed()
}
step("Assert scan card button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
walletNameButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
step("Assert Link more cards button does not exist") {
linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert Card Settings button is visible") {
cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button is visible") {
referralProgramButton.assertIsDisplayed()
}
step("Assert Forget wallet button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
} }
step("Assert wallet connect button is visible") {
DetailsPage.walletConnectButton.assertIsDisplayed()
}
step("Assert scan card button is visible") {
DetailsPage.scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
DetailsPage.buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
DetailsPage.appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
DetailsPage.contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
DetailsPage.toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
DetailsPage.walletNameButton.click()
}
step("Assert Link more cards button does not exist") {
WalletSettingsPage.linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert Card Settings button is visible") {
WalletSettingsPage.cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button is visible") {
WalletSettingsPage.referralProgramButton.assertIsDisplayed()
}
step("Assert Forget wallet button is visible") {
WalletSettingsPage.forgetWalletButton.assertIsDisplayed()
}
}
@Test @Test
fun noteDetails() { fun noteDetails() =
MainPageScenario.open(ProductType.Note) setupHooks().run {
step("Open wallet details") { scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note))
TopBarPage.moreButton.click() ComposeScreen.onComposeScreen<TestTopBar>(composeTestRule) {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<DetailsTestScreen>(composeTestRule) {
step("Assert wallet connect button does not exist") {
walletConnectButton.assertIsNotDisplayed()
}
step("Assert scan card button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
walletNameButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
step("Assert Card Settings button is visible") {
cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button does not exist") {
referralProgramButton.assertIsNotDisplayed()
}
step("Assert Forget wallet button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
} }
step("Assert wallet connect button does not exist") {
DetailsPage.walletConnectButton.assertIsNotDisplayed()
}
step("Assert scan card button is visible") {
DetailsPage.scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
DetailsPage.buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
DetailsPage.appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
DetailsPage.contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
DetailsPage.toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
DetailsPage.walletNameButton.click()
}
step("Assert Card Settings button is visible") {
WalletSettingsPage.cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button does not exist") {
WalletSettingsPage.referralProgramButton.assertIsNotDisplayed()
}
step("Assert Forget wallet button is visible") {
WalletSettingsPage.forgetWalletButton.assertIsDisplayed()
}
}
} }

View file

@ -0,0 +1,21 @@
package com.tangem.tests
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import com.tangem.common.BaseTestCase
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class MainScreenTest : BaseTestCase() {
@Test
fun goToMain() {
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
}
}
}

View file

@ -1,42 +1,40 @@
package com.tangem.tests package com.tangem.tests
import com.atiurin.ultron.allure.step.step
import com.atiurin.ultron.extensions.assertIsDisplayed
import com.atiurin.ultron.extensions.assertIsNotDisplayed
import com.atiurin.ultron.extensions.click
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerPage import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.MainPage import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.StoriesPage import com.tangem.screens.MainTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.domain.sdk.mocks.MockProvider
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
class ScanErrorTest : BaseTestCase() { class ScanErrorTest : BaseTestCase() {
@Test @Test
fun goToMain() { fun goToMain() =
step("Click on \"Accept\" button") { setupHooks().run {
DisclaimerPage.acceptButton.click() ComposeScreen.onComposeScreen<DisclaimerTestScreen>(composeTestRule) {
step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesTestScreen>(composeTestRule) {
step("Click on \"Scan\" button emulating scan error") {
MockProvider.setEmulateError()
scanButton.clickWithAssertion()
}
step("Click on \"Scan\" button again without emulating error") {
MockProvider.resetEmulateError()
scanButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<MainTestScreen>(composeTestRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
} }
step("Emulate scan error") {
MockProvider.setEmulateError()
}
step("Click on \"Scan\" button emulating scan error") {
StoriesPage.scanButton.click()
}
step("Assert: Error is displayed") {
MainPage.container.assertIsNotDisplayed()
}
step("Emulate success scan") {
MockProvider.resetEmulateError()
}
step("Click on \"Scan\" button") {
StoriesPage.scanButton.click()
}
step("Assert: wallet screen is displayed") {
MainPage.container.assertIsDisplayed()
}
}
} }

View file

@ -1,36 +1,41 @@
package com.tangem.tests package com.tangem.tests
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.UriMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.espresso.intent.matcher.IntentMatchers.hasData
import com.atiurin.ultron.allure.step.step
import com.atiurin.ultron.core.config.UltronConfig.UiAutomator.Companion.uiDevice
import com.atiurin.ultron.extensions.click
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerPage import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.StoriesPage import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import org.hamcrest.core.AllOf.allOf import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.kakao.intent.KIntent
import org.hamcrest.Matchers
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
class StoriesTest : BaseTestCase() { class StoriesTest : BaseTestCase() {
@Test @Test
fun checkOrderButton() { fun clickOnOrderButton() =
Intents.init() setupHooks().run {
step("Click Accept on ToS") { ComposeScreen.onComposeScreen<DisclaimerTestScreen>(composeTestRule) {
DisclaimerPage.acceptButton.click() step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesTestScreen>(composeTestRule) {
step("Click on \"Order\" button") {
orderButton.clickWithAssertion()
}
step("Assert: browser opened") {
val expectedIntent = KIntent {
hasAction(ACTION_VIEW)
hasData { toString().startsWith(NEW_BUY_WALLET_URL) }
}
expectedIntent.intended()
device.uiDevice.pressBack()
}
}
} }
step("Click order button ") {
StoriesPage.orderButton.click()
}
step("Assert: browser is opened ") {
Intents.intended(allOf(hasAction(ACTION_VIEW), hasData(NEW_BUY_WALLET_URL)))
uiDevice.pressBack()
Intents.release()
}
}
} }

View file

@ -45,7 +45,14 @@
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true" android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning" tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent, android:label"> tools:replace="android:allowBackup, android:fullBackupContent, android:label, android:largeHeap">
<!--
property to make the "android:screenOrientation" work on API <37
https://developer.android.com/about/versions/16/behavior-changes-16
-->
<property android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
android:value="true" />
<meta-data <meta-data
android:name="com.google.android.gms.wallet.api.enabled" android:name="com.google.android.gms.wallet.api.enabled"

View file

@ -77,6 +77,7 @@ import com.tangem.tap.routing.utils.DeepLinkFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.utils.extensions.uriValidate import com.tangem.utils.extensions.uriValidate
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -353,7 +354,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
intentProcessor.removeAll() intentProcessor.removeAll()
// workaround: kill process when activity destroy to avoid state when lock() wallets // workaround: kill process when activity destroy to avoid state when lock() wallets
// and navigation to unlock screen was skipped because system kills activity but not process // and navigation to unlock screen was skipped because system kills activity but not process
android.os.Process.killProcess(android.os.Process.myPid()) if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) {
android.os.Process.killProcess(android.os.Process.myPid())
}
super.onDestroy() super.onDestroy()
} }
@ -556,5 +559,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
companion object { companion object {
private const val APP_THEME_LOAD_TIMEOUT = 2 private const val APP_THEME_LOAD_TIMEOUT = 2
private const val MOCKED_BUILD_TYPE = "mocked"
} }
} }

View file

@ -3,11 +3,9 @@ package com.tangem.tap.data
import android.content.Context import android.content.Context
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -21,11 +19,9 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
) : VisaAuthTokenStorage { ) : VisaAuthTokenStorage {
private val secureStorage by lazy { private val secureStorage by lazy {
AndroidSecureStorage( AndroidSecureStorageV2(
preferences = SecureStorage.createEncryptedSharedPreferences( appContext = applicationContext,
context = applicationContext, name = "visa_auth_storage",
storageName = "visa_auth_storage",
),
) )
} }

View file

@ -3,11 +3,9 @@ package com.tangem.tap.data
import android.content.Context import android.content.Context
import com.tangem.common.extensions.toByteArray import com.tangem.common.extensions.toByteArray
import com.tangem.common.extensions.toInt import com.tangem.common.extensions.toInt
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.VisaOtpData
import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -21,11 +19,9 @@ class DefaultVisaOTPStorage @Inject constructor(
) : VisaOTPStorage { ) : VisaOTPStorage {
private val secureStorage by lazy { private val secureStorage by lazy {
AndroidSecureStorage( AndroidSecureStorageV2(
preferences = SecureStorage.createEncryptedSharedPreferences( appContext = applicationContext,
context = applicationContext, name = "visa_otp_storage",
storageName = "visa_otp_storage",
),
) )
} }

View file

@ -30,8 +30,10 @@ internal object RootAppComponentContextModule {
componentBuilder: ModelComponent.Builder, componentBuilder: ModelComponent.Builder,
@GlobalUiMessageSender messageSender: UiMessageSender, @GlobalUiMessageSender messageSender: UiMessageSender,
): AppComponentContext { ): AppComponentContext {
val activity = context as AppCompatActivity
return DefaultAppComponentContext( return DefaultAppComponentContext(
componentContext = (context as AppCompatActivity).defaultComponentContext(), componentContext = activity.defaultComponentContext(),
activity = activity,
dispatchers = dispatchers, dispatchers = dispatchers,
hiltComponentBuilder = componentBuilder, hiltComponentBuilder = componentBuilder,
messageSender = messageSender, messageSender = messageSender,

View file

@ -5,7 +5,7 @@ import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.TokensFeatureToggles
@ -36,8 +36,10 @@ internal object ManageTokensDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase { fun provideCreateCryptoCurrencyUseCase(
return CreateCurrencyUseCase(customTokensRepository) customTokensRepository: CustomTokensRepository,
): CreateCryptoCurrencyUseCase {
return CreateCryptoCurrencyUseCase(customTokensRepository)
} }
@Provides @Provides
@ -71,7 +73,7 @@ internal object ManageTokensDomainModule {
derivationsRepository: DerivationsRepository, derivationsRepository: DerivationsRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
): SaveManagedTokensUseCase { ): SaveManagedTokensUseCase {
@ -82,7 +84,7 @@ internal object ManageTokensDomainModule {
derivationsRepository = derivationsRepository, derivationsRepository = derivationsRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
) )

View file

@ -6,8 +6,8 @@ import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
@ -52,8 +52,8 @@ object MarketsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideGetTokenQuotesUseCase(singleQuoteSupplier: SingleQuoteSupplier): GetCurrencyQuotesUseCase { fun provideGetTokenQuotesUseCase(singleQuoteStatusSupplier: SingleQuoteStatusSupplier): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(singleQuoteSupplier = singleQuoteSupplier) return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier)
} }
@Provides @Provides
@ -64,7 +64,7 @@ object MarketsDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
): SaveMarketTokensUseCase { ): SaveMarketTokensUseCase {
@ -74,7 +74,7 @@ object MarketsDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
) )

View file

@ -3,8 +3,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.* import com.tangem.domain.nft.*
import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.quotes.single.SingleQuoteFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -92,18 +92,18 @@ internal object NFTDomainModule {
@Singleton @Singleton
fun provideGetNFTPriceUseCase( fun provideGetNFTPriceUseCase(
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleQuoteSupplier: SingleQuoteSupplier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
): GetNFTPriceUseCase { ): GetNFTPriceUseCase {
return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier) return GetNFTPriceUseCase(nftRepository, singleQuoteStatusSupplier)
} }
@Provides @Provides
@Singleton @Singleton
fun provideFetchNFTPriceUseCase( fun provideFetchNFTPriceUseCase(
nftRepository: NFTRepository, nftRepository: NFTRepository,
singleQuoteFetcher: SingleQuoteFetcher, singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
): FetchNFTPriceUseCase { ): FetchNFTPriceUseCase {
return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) return FetchNFTPriceUseCase(nftRepository, singleQuoteStatusFetcher)
} }
@Provides @Provides

View file

@ -1,12 +1,19 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.swap.SwapErrorResolver
import com.tangem.domain.swap.SwapRepositoryV2
import com.tangem.domain.swap.SwapTransactionRepository
import com.tangem.domain.swap.usecase.GetSwapPairsUseCase
import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase
import com.tangem.feature.swap.domain.api.SwapRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton import javax.inject.Singleton
import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -17,7 +24,53 @@ internal object SwapDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase { fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
return GetAvailablePairsUseCase(swapRepository = swapRepository) return GetAvailablePairsUseCase(swapRepository = swapRepository)
} }
@Provides
@Singleton
fun provideGetSwapSupportedPairsUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapSupportedPairsUseCase {
return GetSwapSupportedPairsUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
@Provides
@Singleton
fun provideGetSwapPairsUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapPairsUseCase {
return GetSwapPairsUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
@Provides
@Singleton
fun provideSelectInitialPairUseCase(
swapTransactionRepository: SwapTransactionRepository,
): SelectInitialPairUseCase {
return SelectInitialPairUseCase(
swapTransactionRepository = swapTransactionRepository,
)
}
@Provides
@Singleton
fun provideGetSwapQuoteUseCase(
swapRepositoryV2: SwapRepositoryV2,
swapErrorResolver: SwapErrorResolver,
): GetSwapQuoteUseCase {
return GetSwapQuoteUseCase(
swapRepositoryV2 = swapRepositoryV2,
swapErrorResolver = swapErrorResolver,
)
}
} }

View file

@ -8,9 +8,9 @@ import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepositoryV2 import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
@ -42,7 +42,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
): AddCryptoCurrenciesUseCase { ): AddCryptoCurrenciesUseCase {
@ -50,7 +50,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
) )
@ -62,7 +62,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
): FetchTokenListUseCase { ): FetchTokenListUseCase {
@ -70,7 +70,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
) )
@ -161,7 +161,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
): FetchCurrencyStatusUseCase { ): FetchCurrencyStatusUseCase {
@ -169,7 +169,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
) )
@ -181,7 +181,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
): FetchCardTokenListUseCase { ): FetchCardTokenListUseCase {
@ -189,7 +189,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
) )
@ -346,11 +346,11 @@ internal object TokensDomainModule {
@Singleton @Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase( fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
): RefreshMultiCurrencyWalletQuotesUseCase { ): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase( return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
) )
} }
@ -368,27 +368,27 @@ internal object TokensDomainModule {
fun provideBaseCurrenciesStatusesOperations( fun provideBaseCurrenciesStatusesOperations(
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepositoryV2: QuotesRepositoryV2, quotesRepository: QuotesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteSupplier: SingleQuoteSupplier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
): BaseCurrenciesStatusesOperations { ): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations( return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
quotesRepositoryV2 = quotesRepositoryV2, quotesRepository = quotesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteSupplier = singleQuoteSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,
@ -400,27 +400,27 @@ internal object TokensDomainModule {
fun provideBaseCurrencyStatusOperations( fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles, tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepositoryV2: QuotesRepositoryV2, quotesRepository: QuotesRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier, singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteSupplier: SingleQuoteSupplier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
): BaseCurrencyStatusOperations { ): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations( return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
quotesRepositoryV2 = quotesRepositoryV2, quotesRepository = quotesRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteSupplier = singleQuoteSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles, tokensFeatureToggles = tokensFeatureToggles,

View file

@ -79,6 +79,18 @@ internal object TransactionDomainModule {
) )
} }
@Provides
@Singleton
fun provideOpenTrustlineUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
): OpenTrustlineUseCase {
return OpenTrustlineUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideDismissIncompleteTransactionUseCase( fun provideDismissIncompleteTransactionUseCase(

View file

@ -13,6 +13,7 @@ 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.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorage
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.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
@ -80,6 +81,10 @@ internal object UserWalletsListManagerModule {
context = applicationContext, context = applicationContext,
storageName = "user_wallets_storage", storageName = "user_wallets_storage",
), ),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
name = "user_wallets_storage2",
),
) )
val authenticatedStorage = AuthenticatedStorage( val authenticatedStorage = AuthenticatedStorage(

View file

@ -83,11 +83,14 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization") Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(
.getOrElse { cardId = card.cardId,
Timber.i("Failed to get Access token for Wallet public key authorization") // This is the wallet public key, not the address and it's alright, as the API expects it in this format
return CompletionResult.Failure(it.tangemError) cardWalletAddress = wallet.publicKey.toHexString(),
} ).getOrElse {
Timber.i("Failed to get Access token for Wallet public key authorization")
return CompletionResult.Failure(it.tangemError)
}
val signChallengeResult = signChallengeWithWallet( val signChallengeResult = signChallengeWithWallet(
publicKey = wallet.publicKey, publicKey = wallet.publicKey,
@ -96,15 +99,16 @@ internal class VisaCardScanHandler @Inject constructor(
return when (signChallengeResult) { return when (signChallengeResult) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
val signature = signChallengeResult.data.cardSignature ?: run { val signature = signChallengeResult.data.walletSignature
Timber.i("Failed to sign challenge with Wallet public key") val salt = signChallengeResult.data.salt
return CompletionResult.Failure(VisaCardScanError.FailedToSignChallenge.tangemError)
}
Timber.i("Challenge signed with Wallet public key") Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens( handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value, cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()), signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = signature.toHexString(),
salt = salt.toHexString(),
),
) )
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.utils.findActivity
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.compose.StoriesScreenV2
import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store import com.tangem.tap.store
@ -57,12 +58,22 @@ internal class DefaultHomeComponent @AssistedInject constructor(
val activity = LocalContext.current.findActivity() val activity = LocalContext.current.findActivity()
BackHandler(onBack = activity::finish) BackHandler(onBack = activity::finish)
SystemBarsIconsDisposable(darkIcons = false) SystemBarsIconsDisposable(darkIcons = false)
StoriesScreen( if (homeState.value.isV2StoriesEnabled) {
homeState = homeState, StoriesScreenV2(
onScanButtonClick = model::onScanClick, homeState = homeState,
onShopButtonClick = model::onShopClick, onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,
onSearchTokensClick = model::onSearchClick, onAddExistingWalletButtonClick = model::onAddExistingWalletScreen,
) onScanButtonClick = model::onScanClick,
)
} else {
StoriesScreen(
homeState = homeState,
onScanButtonClick = model::onScanClick,
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
}
ChangeRootBackgroundColorEffect(Color(color = 0xFF010101)) ChangeRootBackgroundColorEffect(Color(color = 0xFF010101))
} }

View file

@ -71,6 +71,14 @@ internal class HomeModel @Inject constructor(
.launchIn(modelScope) .launchIn(modelScope)
} }
fun onCreateNewWalletScreen() {
// TODO implement navigation to create new wallet
}
fun onAddExistingWalletScreen() {
// TODO implement navigation to add existing wallet
}
fun onScanClick() { fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
scanCard() scanCard()

View file

@ -0,0 +1,252 @@
@file:Suppress("MagicNumber")
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtonsV2
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.Stories
import com.tangem.wallet.R
import kotlin.math.max
@Composable
internal fun StoriesScreenV2(
homeState: MutableState<HomeState>,
onCreateNewWalletButtonClick: () -> Unit,
onAddExistingWalletButtonClick: () -> Unit,
onScanButtonClick: () -> Unit,
) {
val state = homeState.value
var currentStory by remember { mutableStateOf(state.firstStory) }
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
}
val goToNextStory = remember(currentStory, currentStoryIndex) {
{
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
state.stories[currentStoryIndex + 1]
} else {
state.firstStory
}
}
}
// todo refactor [REDACTED_TASK_KEY]
StoriesScreenContentV2(
modifier = Modifier
.fillMaxSize()
.testTag(TestTags.STORIES_SCREEN),
config = StoriesScreenContentV2Config(
storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex,
currentStory = currentStory,
isScanInProgress = homeState.value.scanInProgress,
onGoToPreviousStory = goToPreviousStory,
onGoToNextStory = goToNextStory,
onCreateNewWalletButtonClick = onCreateNewWalletButtonClick,
onAddExistingWalletButtonClick = onAddExistingWalletButtonClick,
onScanButtonClick = onScanButtonClick,
),
)
}
@Deprecated("Use StoriesContainer from core/ui")
@Suppress("LongMethod")
@Composable
private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifier: Modifier = Modifier) {
var isPressed by remember { mutableStateOf(value = false) }
val isPaused = isPressed || config.isScanInProgress
val currentStoryDuration = config.currentStory.duration
Box(
modifier = modifier.background(Color(0xFF010101)),
) {
Row(
modifier = Modifier.fillMaxSize(),
) {
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) config.onGoToPreviousStory()
isPressed = false
},
)
},
)
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(Unit) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) config.onGoToNextStory()
isPressed = false
},
)
},
)
}
Column(
modifier = Modifier
.statusBarsPadding()
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
StoriesProgressBar(
steps = config.storiesSize,
currentStep = config.currentStoryIndex,
stepDuration = currentStoryDuration,
paused = isPaused,
onStepFinish = config.onGoToNextStory,
)
Image(
painter = painterResource(id = R.drawable.ic_tangem_logo),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing16,
)
.height(TangemTheme.dimens.size18)
.align(Alignment.Start),
)
when (config.currentStory) {
Stories.TangemIntro -> FirstStoriesContent(
isPaused = isPaused,
duration = currentStoryDuration,
)
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
isPaused = isPaused,
stepDuration = currentStoryDuration,
)
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
}
}
Column(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing16)
.align(Alignment.BottomCenter)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
HomeButtonsV2(
modifier = Modifier.fillMaxWidth(),
btnScanStateInProgress = config.isScanInProgress,
onScanButtonClick = config.onScanButtonClick,
onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick,
onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick,
)
}
}
}
private data class StoriesScreenContentV2Config(
val storiesSize: Int,
val currentStoryIndex: Int,
val currentStory: Stories,
val isScanInProgress: Boolean,
val onGoToPreviousStory: () -> Unit = {},
val onGoToNextStory: () -> Unit = {},
val onCreateNewWalletButtonClick: () -> Unit = {},
val onAddExistingWalletButtonClick: () -> Unit = {},
val onScanButtonClick: () -> Unit = {},
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun StoriesScreenContentV2Preview(
@PreviewParameter(StoriesScreenContentV2ConfigProvider::class) config: StoriesScreenContentV2Config,
) {
TangemThemePreview {
StoriesScreenContentV2(config = config)
}
}
private class StoriesScreenContentV2ConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentV2Config>(
collection = listOf(
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 0,
currentStory = Stories.TangemIntro,
isScanInProgress = true,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 1,
currentStory = Stories.RevolutionaryWallet,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 2,
currentStory = Stories.UltraSecureBackup,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 3,
currentStory = Stories.Currencies,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 4,
currentStory = Stories.Web3,
isScanInProgress = false,
),
StoriesScreenContentV2Config(
storiesSize = 6,
currentStoryIndex = 5,
currentStory = Stories.WalletForEveryone,
isScanInProgress = false,
),
),
)
// endregion Preview

View file

@ -0,0 +1,124 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
@Composable
internal fun HomeButtonsV2(
btnScanStateInProgress: Boolean,
onScanButtonClick: () -> Unit,
onCreateNewWalletButtonClick: () -> Unit,
onAddExistingWalletButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
CreateNewWalletButton(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON),
onClick = onCreateNewWalletButtonClick,
)
AddExistingWalletButton(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON),
onClick = onAddExistingWalletButtonClick,
)
ScanCardButton(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON),
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)
}
}
@Composable
private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_create_new_wallet),
useDarkerColors = false,
onClick = onClick,
)
}
@Composable
private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_add_existing_wallet),
useDarkerColors = true,
onClick = onClick,
)
}
@Composable
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResourceSafe(id = R.string.home_button_scan),
useDarkerColors = true,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
onClick = onClick,
showProgress = showProgress,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) {
TangemThemePreview {
Box(
modifier = Modifier.background(Color.Black),
) {
HomeButtonsV2(
btnScanStateInProgress = state.btnScanStateInProgress,
onCreateNewWalletButtonClick = {},
onAddExistingWalletButtonClick = {},
onScanButtonClick = {},
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
)
}
}
}
private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider<HomeButtonsV2State>(
collection = listOf(
HomeButtonsV2State(
btnScanStateInProgress = false,
),
HomeButtonsV2State(
btnScanStateInProgress = true,
),
),
)
private data class HomeButtonsV2State(
val btnScanStateInProgress: Boolean,
)
// endregion Preview

View file

@ -7,6 +7,7 @@ import org.rekotlin.StateType
// todo refactor [REDACTED_TASK_KEY] // todo refactor [REDACTED_TASK_KEY]
data class HomeState( data class HomeState(
val scanInProgress: Boolean = false, val scanInProgress: Boolean = false,
val isV2StoriesEnabled: Boolean = false,
val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(), val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(),
) : StateType { ) : StateType {

View file

@ -1,54 +0,0 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ENV ANDROID_HOME=/opt/android-sdk
ENV BUNDLE_PATH=vendor/bundle
ENV PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools/34.0.0:$BUNDLE_PATH/bin
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
RUN apt-get update && apt-get install -y \
locales \
openjdk-17-jdk \
wget \
unzip \
curl \
git \
ruby \
ruby-dev \
build-essential \
jq \
&& locale-gen en_US.UTF-8 \
&& update-locale LANG=en_US.UTF-8 \
&& apt-get clean
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \
apt-get update && apt-get install -y gh && \
apt-get clean
RUN mkdir -p $ANDROID_HOME/cmdline-tools/latest && \
wget https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/cmdline-tools.zip && \
unzip /tmp/cmdline-tools.zip -d $ANDROID_HOME/cmdline-tools/latest && \
mv $ANDROID_HOME/cmdline-tools/latest/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ && \
rm -rf $ANDROID_HOME/cmdline-tools/latest/cmdline-tools && \
rm -f /tmp/cmdline-tools.zip && \
yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses && \
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-31" "platforms;android-34" "build-tools;34.0.0"
RUN wget https://github.com/lzhiyong/android-sdk-tools/releases/download/34.0.3/android-sdk-tools-static-aarch64.zip -O /tmp/android-sdk-tools-static-aarch64.zip && \
unzip /tmp/android-sdk-tools-static-aarch64.zip -d /tmp/android-sdk-tools-static-arm && \
cp -r /tmp/android-sdk-tools-static-arm/build-tools/* $ANDROID_HOME/build-tools/34.0.0/ && \
rm -rf /tmp/android-sdk-tools-static-arm /tmp/android-sdk-tools-static-aarch64.zip
RUN gem install bundler:2.5.23
RUN gem install fastlane -v 2.225.0 -N -V
RUN gem install fastlane-plugin-firebase_app_distribution -v 0.9.1 -N -V
COPY ../Gemfile Gemfile.lock ./
RUN bundle install --jobs=4 --retry=3 --verbose
RUN bundle exec fastlane -v
CMD ["bash"]

View file

@ -1,26 +0,0 @@
FROM ubuntu:24.04
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
locales \
curl \
git \
&& locale-gen en_US.UTF-8 \
&& update-locale LANG=en_US.UTF-8 \
&& apt-get clean
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \
apt-get update && apt-get install -y gh && \
apt-get clean
RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get install -y git-lfs && \
git lfs install && \
apt-get clean
CMD ["bash"]

View file

@ -1,13 +0,0 @@
org.gradle.jvmargs=-Xmx7g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.workers.max=8
org.gradle.daemon=false
org.gradle.unsafe.configuration-cache=false
org.gradle.console=plain
org.gradle.caching=false
android.useAndroidX=true
android.nonTransitiveRClass=false
android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/34.0.0/aapt2
kotlin.incremental=true

View file

@ -1,63 +0,0 @@
#!/bin/bash
set -eo pipefail
# Input args
if [ -z "$1" ]; then
echo "Error: No base branch argument provided or empty string given."
echo "Usage: $0 <base-branch>"
exit 1
fi
base_branch=$1
# Initialize arrays to store branch-commit mapping
declare -a branches
declare -a commits
# Source refs to find branches from
local_refs="refs/heads/releases/*" # For debug and development
#TODO remove pre_release branches matching after 5.21 release
remote_refs=$(git for-each-ref --sort=-committerdate --format="%(refname:short)" refs/remotes/origin/ | grep -E "origin/([a-zA-Z0-9._-]+_pre_release|releases/[a-zA-Z0-9._-]+)$")
# Iterate over all remote release branches, sorted by commit date (most recent first)
index=0
for branch in $remote_refs; do
# Find the latest commit that is an ancestor of base_branch using --first-parent strategy
candidate_commit=$(git rev-list --first-parent "$branch..$base_branch" | tail -1)
if [ -n "$candidate_commit" ]; then
branches[$index]="$branch"
commits[$index]="$candidate_commit"
index=$((index + 1))
fi
done
# Debug output of branches and commits
echo "Branches and their corresponding commits:" >&2
for i in "${!branches[@]}"; do
echo "${branches[$i]} -> ${commits[$i]}" >&2
done
# Find the branch with the most recent commit
latest_branch=""
latest_commit=""
for i in "${!branches[@]}"; do
branch=${branches[$i]}
commit=${commits[$i]}
if [ -z "$latest_commit" ] || [ "$(git rev-list --count $latest_commit..$commit)" -gt 0 ]; then
latest_branch=$branch
latest_commit=$commit
fi
done
# Output validation
if [ -z "$latest_branch" ]; then
echo "Error: Can't find the latest 'release/*' branch for the base branch '$base_branch'" >&2
exit 2
fi
# Stripping 'origin/' prefix if needed
latest_branch="${latest_branch#origin/}"
echo "$latest_branch" > "find-latest-release-branch.output"
echo "Latest release branch created directly from '$base_branch' or its ancestor: '$latest_branch'"

View file

@ -1,13 +0,0 @@
org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=512m -XX:+UseParallelGC -XX:ParallelGCThreads=3 -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.workers.max=3
org.gradle.daemon=false
org.gradle.unsafe.configuration-cache=false
org.gradle.console=plain
org.gradle.caching=false
android.useAndroidX=true
android.nonTransitiveRClass=false
android.aapt2FromMavenOverride=/opt/android-sdk/build-tools/34.0.0/aapt2
kotlin.incremental=false

View file

@ -33,4 +33,5 @@ dependencies {
implementation(tangemDeps.card.core) implementation(tangemDeps.card.core)
implementation(deps.test.junit5) implementation(deps.test.junit5)
implementation(deps.test.truth)
} }

View file

@ -1,14 +1,22 @@
package com.tangem.common.test.data.quote package com.tangem.common.test.data.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.quote.converter.QuoteConverter
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Quote import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.quote.QuoteStatus
import com.tangem.utils.extensions.orZero
fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): Quote { fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus {
return QuoteConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first()) return QuoteStatus(
rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId),
value = QuoteStatus.Data(
source = source,
fiatRate = price.orZero(),
priceChange = priceChange24h.orZero().movePointLeft(2),
),
)
} }
fun Pair<String, QuotesResponse.Quote>.toDomain(source: StatusSource = StatusSource.ACTUAL): Quote { fun Pair<String, QuotesResponse.Quote>.toDomain(source: StatusSource = StatusSource.ACTUAL): QuoteStatus {
return QuoteConverter(source = source).convert(value = mapOf(this).entries.first()) return second.toDomain(rawCurrencyId = first, source = source)
} }

View file

@ -0,0 +1,14 @@
package com.tangem.common.test.utils
import arrow.core.Either
import com.google.common.truth.Truth
fun <B> assertEither(actual: Either<Throwable, B>, expected: Either<Throwable, B>) {
actual
.onRight { Truth.assertThat(it).isEqualTo(expected) }
.onLeft {
val expectedError = expected.leftOrNull() ?: error("Expected must be Either.Left")
Truth.assertThat(it::class.java).isEqualTo(expectedError::class.java)
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.common.ui.amountScreen.ui.amountField import com.tangem.common.ui.amountScreen.ui.amountField
import com.tangem.common.ui.amountScreen.ui.amountFieldV2
import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.common.ui.amountScreen.ui.buttons
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
@ -40,18 +41,28 @@ fun AmountScreenContent(
bottom = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16,
), ),
) { ) {
amountField( if (amountState.isRedesignEnabled) {
amountState = amountState, amountFieldV2(
isBalanceHidden = isBalanceHidden, amountState = amountState,
onValueChange = clickIntents::onAmountValueChange, onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
) onCurrencyChange = clickIntents::onCurrencyChangeClick,
buttons( onMaxAmountClick = clickIntents::onMaxValueClick,
segmentedButtonConfig = amountState.segmentedButtonConfig, )
clickIntents = clickIntents, } else {
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, amountField(
selectedButton = amountState.selectedButton, amountState = amountState,
) isBalanceHidden = isBalanceHidden,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
selectedButton = amountState.selectedButton,
)
}
} }
} }
@ -75,6 +86,7 @@ private class SendAmountContentPreviewProvider : PreviewParameterProvider<Amount
override val values: Sequence<AmountState> override val values: Sequence<AmountState>
get() = sequenceOf( get() = sequenceOf(
AmountStatePreviewData.amountState, AmountStatePreviewData.amountState,
AmountStatePreviewData.amountStateV2,
) )
} }
// endregion // endregion

View file

@ -58,10 +58,11 @@ class AmountStateConverter(
return AmountState.Data( return AmountState.Data(
title = value.title, title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
tokenName = stringReference(status.currency.name),
tokenIconState = iconStateConverter.convert(status), tokenIconState = iconStateConverter.convert(status),
amountTextField = amountFieldConverter.convert(value.value), amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
appCurrencyCode = appCurrency.code, appCurrency = appCurrency,
segmentedButtonConfig = persistentListOf( segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig( AmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol), title = stringReference(status.currency.symbol),
@ -80,6 +81,7 @@ class AmountStateConverter(
), ),
isSegmentedButtonsEnabled = !noFeeRate, isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0, selectedButton = 0,
isRedesignEnabled = false,
) )
} }
} }
@ -99,6 +101,7 @@ class AmountStateConverterV2(
private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val maxEnterAmount: EnterAmountBoundary, private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter, private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val isRedesignEnabled: Boolean,
) : Converter<AmountParameters, AmountState> { ) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -116,11 +119,16 @@ class AmountStateConverterV2(
return AmountState.Data( return AmountState.Data(
title = value.title, title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), availableBalance = if (isRedesignEnabled) {
resourceReference(R.string.common_balance, wrappedList(crypto))
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
},
tokenName = stringReference(cryptoCurrencyStatus.currency.name),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus), tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus),
amountTextField = amountFieldConverter.convert(value.value), amountTextField = amountFieldConverter.convert(value.value),
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
appCurrencyCode = appCurrency.code, appCurrency = appCurrency,
segmentedButtonConfig = persistentListOf( segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig( AmountSegmentedButtonsConfig(
title = stringReference(cryptoCurrencyStatus.currency.symbol), title = stringReference(cryptoCurrencyStatus.currency.symbol),
@ -139,6 +147,7 @@ class AmountStateConverterV2(
), ),
isSegmentedButtonsEnabled = !noFeeRate, isSegmentedButtonsEnabled = !noFeeRate,
selectedButton = 0, selectedButton = 0,
isRedesignEnabled = isRedesignEnabled,
) )
} }
} }

View file

@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.models
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal import java.math.BigDecimal
@ -21,7 +22,7 @@ sealed class AmountState {
* @param selectedButton selected currency index * @param selectedButton selected currency index
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled * @param isSegmentedButtonsEnabled indicates if currency switches is enabled
* @param amountTextField amount field state * @param amountTextField amount field state
* @param appCurrencyCode app currency code * @param appCurrency app currency
* @param isEditingDisabled indicated whether amount is editable * @param isEditingDisabled indicated whether amount is editable
* @param reduceAmountBy reduces amount to be sent by specified value * @param reduceAmountBy reduces amount to be sent by specified value
* @param isIgnoreReduce ignores reduce amount value * @param isIgnoreReduce ignores reduce amount value
@ -30,15 +31,17 @@ sealed class AmountState {
override val isPrimaryButtonEnabled: Boolean, override val isPrimaryButtonEnabled: Boolean,
val title: TextReference, val title: TextReference,
val availableBalance: TextReference, val availableBalance: TextReference,
val tokenName: TextReference,
val tokenIconState: CurrencyIconState, val tokenIconState: CurrencyIconState,
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>, val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
val selectedButton: Int, val selectedButton: Int,
val isSegmentedButtonsEnabled: Boolean, val isSegmentedButtonsEnabled: Boolean,
val amountTextField: AmountFieldModel, val amountTextField: AmountFieldModel,
val appCurrencyCode: String, val appCurrency: AppCurrency,
val isEditingDisabled: Boolean = false, val isEditingDisabled: Boolean = false,
val reduceAmountBy: BigDecimal = BigDecimal.ZERO, val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
val isIgnoreReduce: Boolean = false, val isIgnoreReduce: Boolean = false,
val isRedesignEnabled: Boolean,
) : AmountState() ) : AmountState()
data class Empty( data class Empty(

View file

@ -2,12 +2,16 @@ package com.tangem.common.ui.amountScreen.preview
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.AmountType
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -15,6 +19,8 @@ import java.math.BigDecimal
object AmountStatePreviewData { object AmountStatePreviewData {
val emptyState = AmountState.Empty()
val amountState = AmountState.Data( val amountState = AmountState.Data(
isPrimaryButtonEnabled = false, isPrimaryButtonEnabled = false,
title = stringReference("Family Wallet"), title = stringReference("Family Wallet"),
@ -31,7 +37,8 @@ object AmountStatePreviewData {
isFiat = true, isFiat = true,
), ),
), ),
appCurrencyCode = "usd", appCurrency = AppCurrency.Default,
tokenName = stringReference("Tether"),
amountTextField = AmountFieldModel( amountTextField = AmountFieldModel(
value = "", value = "",
onValueChange = {}, onValueChange = {},
@ -60,6 +67,7 @@ object AmountStatePreviewData {
), ),
isSegmentedButtonsEnabled = true, isSegmentedButtonsEnabled = true,
selectedButton = 0, selectedButton = 0,
isRedesignEnabled = false,
) )
val amountWithValueState = amountState.copy( val amountWithValueState = amountState.copy(
@ -75,6 +83,11 @@ object AmountStatePreviewData {
), ),
) )
val amountStateV2 = amountState.copy(
isRedesignEnabled = true,
availableBalance = resourceReference(R.string.common_balance, wrappedList("2 130,88 USDT")),
)
val amountWithValueFiatState = amountWithValueState.copy( val amountWithValueFiatState = amountWithValueState.copy(
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false), amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
) )

View file

@ -36,7 +36,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
val fiatAmount = amount.fiatAmount.value.format { val fiatAmount = amount.fiatAmount.value.format {
fiat( fiat(
fiatCurrencySymbol = amount.fiatAmount.currencySymbol, fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
fiatCurrencyCode = amountState.appCurrencyCode, fiatCurrencyCode = amountState.appCurrency.code,
) )
} }

View file

@ -0,0 +1,180 @@
package com.tangem.common.ui.amountScreen.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun AmountBlockV2(
amountState: AmountState,
currencyIconState: CurrencyIconState,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (amountState !is AmountState.Data) return
val amount = amountState.amountTextField
val cryptoAmount = amount.cryptoAmount.value?.format {
crypto(
symbol = "",
decimals = amount.cryptoAmount.decimals,
)
}.orEmpty()
val fiatAmount = amount.fiatAmount.value.format {
fiat(
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
fiatCurrencyCode = amountState.appCurrency.code,
)
}
val (firstAmount, secondAmount) = if (amount.isFiatValue) {
fiatAmount to cryptoAmount
} else {
cryptoAmount to fiatAmount
}
val title = TextReference.Str(
stringResourceSafe(
R.string.send_from_wallet_name,
amountState.title
.resolveReference(),
),
)
val currencyTitle = amount.cryptoAmount.currencySymbol
AmountBlockV2(
title = title,
balance = amountState.availableBalance,
currencyTitle = currencyTitle,
currencyIconState = currencyIconState,
firstAmount = firstAmount,
secondAmount = secondAmount,
isClickDisabled = isClickDisabled,
isEditingDisabled = isEditingDisabled,
onClick = onClick,
modifier = modifier,
)
}
@Suppress("LongParameterList")
@Composable
internal fun AmountBlockV2(
title: TextReference,
balance: TextReference,
currencyTitle: String,
currencyIconState: CurrencyIconState,
firstAmount: String,
secondAmount: String,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing16),
) {
Row {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Spacer(modifier = modifier.weight(1f))
Text(
text = balance.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(top = 8.dp),
) {
Text(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 6.dp),
) {
CurrencyIcon(
state = currencyIconState,
modifier = Modifier.padding(horizontal = 14.dp),
)
Text(
text = currencyTitle,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
}
}
}
}
// region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AmountBlockPreview(@PreviewParameter(AmountBlockV2PreviewProvider::class) value: AmountState) {
TangemThemePreview {
AmountBlockV2(
amountState = value,
currencyIconState = CurrencyIconState.Empty(),
isClickDisabled = false,
isEditingDisabled = false,
onClick = {},
)
}
}
private class AmountBlockV2PreviewProvider : PreviewParameterProvider<AmountState> {
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.amountState,
)
}
// endregion

View file

@ -31,56 +31,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@Deprecated("Use AmountField with clicks")
@Composable
internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) {
val decimalFormat = rememberDecimalFormat()
val isFiatValue = amountField.isFiatValue
val currencyCode = if (isFiatValue) appCurrencyCode else null
val (primaryAmount, primaryValue) = if (isFiatValue) {
amountField.fiatAmount to amountField.fiatValue
} else {
amountField.cryptoAmount to amountField.value
}
val requester = remember { FocusRequester() }
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
visualTransformation = AmountVisualTransformation(
decimals = primaryAmount.decimals,
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
),
onValueChange = amountField.onValueChange,
keyboardOptions = amountField.keyboardOptions,
keyboardActions = amountField.keyboardActions,
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
isAutoResize = true,
isValuePasted = amountField.isValuePasted,
onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss,
modifier = Modifier
.focusRequester(requester)
.padding(
top = TangemTheme.dimens.spacing24,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
)
.requiredHeightIn(min = TangemTheme.dimens.size32),
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
AmountSecondary(amountField, appCurrencyCode)
}
@Composable @Composable
internal fun AmountField( internal fun AmountField(
amountField: AmountFieldModel, amountField: AmountFieldModel,
@ -97,7 +47,7 @@ internal fun AmountField(
amountField.cryptoAmount to amountField.value amountField.cryptoAmount to amountField.value
} }
val requester = remember { FocusRequester() } val requester = remember { FocusRequester() }
val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1
AmountTextField( AmountTextField(
value = primaryValue, value = primaryValue,
decimals = primaryAmount.decimals, decimals = primaryAmount.decimals,
@ -106,6 +56,7 @@ internal fun AmountField(
symbol = primaryAmount.currencySymbol, symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode, currencyCode = currencyCode,
decimalFormat = decimalFormat, decimalFormat = decimalFormat,
symbolColor = symbolColor,
), ),
onValueChange = onValueChange, onValueChange = onValueChange,
keyboardOptions = amountField.keyboardOptions, keyboardOptions = amountField.keyboardOptions,

View file

@ -2,20 +2,32 @@ package com.tangem.common.ui.amountScreen.ui
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
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.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
private const val AMOUNT_FIELD_KEY = "amountFieldKey" private const val AMOUNT_FIELD_KEY = "amountFieldKey"
@ -64,10 +76,142 @@ internal fun LazyListScope.amountField(
) )
AmountField( AmountField(
amountField = amountState.amountTextField, amountField = amountState.amountTextField,
appCurrencyCode = amountState.appCurrencyCode, appCurrencyCode = amountState.appCurrency.code,
onValueChange = onValueChange, onValueChange = onValueChange,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
) )
} }
} }
} }
internal fun LazyListScope.amountFieldV2(
amountState: AmountState,
modifier: Modifier = Modifier,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
onCurrencyChange: (Boolean) -> Unit,
onMaxAmountClick: () -> Unit,
) {
item(key = AMOUNT_FIELD_KEY) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
.padding(top = 48.dp, bottom = 28.dp),
) {
if (amountState !is AmountState.Data) {
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier.width(60.dp),
)
} else {
Text(
text = amountState.title.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
AmountFieldV2(
amountUM = amountState,
onValueChange = onValueChange,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
onCurrencyChange = onCurrencyChange,
modifier = Modifier,
)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = 12.dp),
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
)
AmountInfo(
amountUM = amountState,
onMaxAmountClick = onMaxAmountClick,
)
}
}
}
@Composable
private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modifier: Modifier = Modifier) {
val tokenIconState = (amountUM as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier,
) {
CurrencyIcon(
state = tokenIconState,
shouldDisplayNetwork = true,
modifier = Modifier.padding(
start = 16.dp,
top = 16.dp,
bottom = 16.dp,
),
)
AmountInfoMain(amountUM = amountUM)
SpacerWMax()
Text(
text = stringResourceSafe(R.string.send_max_amount),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(end = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = 12.dp, vertical = 4.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = onMaxAmountClick,
),
)
}
}
@Composable
private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = amountUM !is AmountState.Data,
modifier = modifier,
) { isContent ->
if (isContent) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
TextShimmer(
style = TangemTheme.typography.subtitle2,
modifier = Modifier.width(56.dp),
)
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier.width(72.dp),
)
}
} else {
val amountUM = amountUM as AmountState.Data
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = amountUM.tokenName.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
EllipsisText(
text = amountUM.availableBalance.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.cryptoAmount.currencySymbol.length),
)
}
}
}
}

View file

@ -0,0 +1,329 @@
package com.tangem.common.ui.amountScreen.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
private const val ROTATED_DEGREE = 180f
private const val INITIAL_DEGREE = 0f
@Composable
fun AmountFieldV2(
amountUM: AmountState,
onValueChange: (String) -> Unit,
onValuePastedTriggerDismiss: () -> Unit,
onCurrencyChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val decimalFormat = rememberDecimalFormat()
val requester = remember { FocusRequester() }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier,
) {
if (amountUM is AmountState.Empty) {
TextShimmer(
style = TangemTheme.typography.head,
modifier = Modifier.width(150.dp),
)
} else if (amountUM is AmountState.Data) {
AnimatedContent(
targetState = amountUM.amountTextField.isFiatValue,
transitionSpec = { primaryFieldCurrencyAnimation() },
label = "Primary field change animation",
) { isFiatValue ->
val currencyCode = if (isFiatValue) amountUM.appCurrency.code else null
val (primaryAmount, primaryValue) = if (isFiatValue) {
amountUM.amountTextField.fiatAmount to amountUM.amountTextField.fiatValue
} else {
amountUM.amountTextField.cryptoAmount to amountUM.amountTextField.value
}
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
visualTransformation = AmountVisualTransformation(
decimals = primaryAmount.decimals,
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
symbolColor = TangemTheme.colors.text.disabled,
),
onValueChange = onValueChange,
keyboardOptions = amountUM.amountTextField.keyboardOptions,
keyboardActions = amountUM.amountTextField.keyboardActions,
textStyle = TangemTheme.typography.head.copy(
textAlign = TextAlign.Center,
),
isAutoResize = true,
isValuePasted = amountUM.amountTextField.isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
modifier = Modifier
.focusRequester(requester)
.requiredHeightIn(min = 44.dp),
)
LaunchedEffect(key1 = Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
}
}
AmountSecondary(
amountUM = amountUM,
onCurrencyChange = onCurrencyChange,
)
}
}
@Composable
private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit) {
Box(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.padding(top = 8.dp),
) {
if (amountUM is AmountState.Empty) {
TextShimmer(
style = TangemTheme.typography.caption2,
modifier = Modifier
.width(90.dp)
.padding(bottom = 20.dp)
.align(TopCenter),
)
} else if (amountUM is AmountState.Data) {
AmountFieldCurrencyInfo(
amountUM = amountUM,
onCurrencyChange = onCurrencyChange,
)
AmountFieldError(
isError = amountUM.amountTextField.isError,
isWarning = amountUM.amountTextField.isWarning,
error = amountUM.amountTextField.error,
modifier = Modifier
.align(BottomCenter)
.padding(top = 20.dp),
)
}
}
}
@Composable
private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.align(TopCenter)
.padding(bottom = 20.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) },
),
) {
val iconRotateState by animateFloatAsState(
targetValue = if (amountUM.amountTextField.isFiatValue) ROTATED_DEGREE else INITIAL_DEGREE,
animationSpec = tween(
durationMillis = 400,
easing = FastOutSlowInEasing,
),
label = "Currency change icon animation",
)
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_exchange_vertical_24),
),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.graphicsLayer {
rotationY = iconRotateState
},
)
AmountFieldCurrencyIcon(
amountUM = amountUM,
)
}
}
@Composable
private fun AmountFieldCurrencyIcon(amountUM: AmountState.Data) {
AnimatedContent(
targetState = amountUM.amountTextField.isFiatValue,
transitionSpec = { secondaryFieldCurrencyAnimation() },
label = "Secondary field change animation",
) { isFiatValue ->
val secondaryAmount =
if (isFiatValue) amountUM.amountTextField.cryptoAmount else amountUM.amountTextField.fiatAmount
val text = secondaryAmount.value.format {
if (isFiatValue) {
crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals)
} else {
fiat(
fiatCurrencySymbol = amountUM.appCurrency.symbol,
fiatCurrencyCode = amountUM.appCurrency.code,
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = text,
style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
if (isFiatValue) {
CurrencyIcon(
state = amountUM.tokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier.size(12.dp),
)
} else {
FiatIcon(
url = amountUM.appCurrency.iconSmallUrl,
size = 12.dp,
isGrayscale = false,
modifier = Modifier.size(12.dp),
)
}
}
}
}
@Composable
private fun AmountFieldError(
isError: Boolean,
isWarning: Boolean,
error: TextReference,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isError || isWarning,
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier,
label = "Error field appearance animation",
) {
val errorText = remember(this, error) { error }
val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention
Text(
text = errorText.resolveReference(),
style = TangemTheme.typography.caption2,
color = color,
textAlign = TextAlign.Center,
)
}
}
private fun <S> AnimatedContentTransitionScope<S>.secondaryFieldCurrencyAnimation(): ContentTransform {
return (
fadeIn(
animationSpec = tween(durationMillis = 300),
) + scaleIn(
animationSpec = tween(durationMillis = 300),
initialScale = 0.9f,
)
).togetherWith(
fadeOut(
animationSpec = tween(durationMillis = 300),
) + scaleOut(
animationSpec = tween(durationMillis = 300),
targetScale = 0.9f,
),
)
}
private fun <S> AnimatedContentTransitionScope<S>.primaryFieldCurrencyAnimation(): ContentTransform {
return (
fadeIn(
animationSpec = tween(durationMillis = 300),
) + scaleIn(
animationSpec = tween(durationMillis = 300),
initialScale = 0.9f,
) + slideInVertically(initialOffsetY = { it / 2 })
)
.togetherWith(
fadeOut(
animationSpec = tween(durationMillis = 300),
) + scaleOut(
animationSpec = tween(durationMillis = 300),
targetScale = 0.9f,
) + slideOutVertically(targetOffsetY = { it / 2 }),
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun AmountFieldV2_Preview(@PreviewParameter(AmountFieldV2PreviewProvider::class) params: AmountState) {
TangemThemePreview {
AmountFieldV2(
amountUM = params,
onValueChange = {},
onValuePastedTriggerDismiss = { },
onCurrencyChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private class AmountFieldV2PreviewProvider : PreviewParameterProvider<AmountState> {
override val values: Sequence<AmountState>
get() = sequenceOf(
AmountStatePreviewData.emptyState,
AmountStatePreviewData.amountState,
)
}
// endregion

View file

@ -13,12 +13,18 @@ fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurr
return stringReference(formattedFiat) return stringReference(formattedFiat)
} }
fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { fun getFiatString(
value: BigDecimal?,
rate: BigDecimal?,
appCurrency: AppCurrency,
approximate: Boolean = false,
): String {
if (value == null || rate == null) return EMPTY_BALANCE_SIGN if (value == null || rate == null) return EMPTY_BALANCE_SIGN
val feeValue = value.multiply(rate) val feeValue = value.multiply(rate)
return BigDecimalFormatter.formatFiatAmount( return BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeValue, fiatAmount = feeValue,
fiatCurrencyCode = appCurrency.code, fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol, fiatCurrencySymbol = appCurrency.symbol,
withApproximateSign = approximate,
) )
} }

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
@Suppress("CyclomaticComplexMethod")
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference { fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
return when (val unavailabilityReason = this) { return when (val unavailabilityReason = this) {
is ScenarioUnavailabilityReason.StakingUnavailable -> { is ScenarioUnavailabilityReason.StakingUnavailable -> {
@ -46,6 +47,9 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message, id = R.string.warning_receive_blocked_hedera_token_association_required_message,
) )
ScenarioUnavailabilityReason.TrustlineRequired -> resourceReference(
id = R.string.warning_receive_blocked_token_trustline_required_message,
)
ScenarioUnavailabilityReason.UsedOutdatedData -> { ScenarioUnavailabilityReason.UsedOutdatedData -> {
resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance) resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance)
} }

View file

@ -166,7 +166,7 @@ private fun NameAndInfo(
@Suppress("MagicNumber") @Suppress("MagicNumber")
@Composable @Composable
private fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modifier) { fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modifier) {
val imageModifier = modifier val imageModifier = modifier
.width(TangemTheme.dimens.size36) .width(TangemTheme.dimens.size36)
.height(TangemTheme.dimens.size24) .height(TangemTheme.dimens.size24)

View file

@ -219,6 +219,7 @@ sealed class AnalyticsParam {
const val ACTION = "Action" const val ACTION = "Action"
const val COLLECTIONS = "Collections" const val COLLECTIONS = "Collections"
const val NFT = "Nft" const val NFT = "Nft"
const val NONCE = "Nonce"
const val STANDARD = "Standard" const val STANDARD = "Standard"
const val NO_COLLECTION = "No collection" const val NO_COLLECTION = "No collection"
} }

View file

@ -62,5 +62,17 @@
{ {
"name": "USEDESK_ENABLED", "name": "USEDESK_ENABLED",
"version": "undefined" "version": "undefined"
},
{
"name": "SEND_VIA_SWAP_ENABLED",
"version": "undefined"
},
{
"name": "SWAP_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "SEND_REDESIGN_ENABLED",
"version": "undefined"
} }
] ]

View file

@ -28,9 +28,9 @@ internal class DefaultVersionProvider @Inject constructor(
context.packageName, context.packageName,
PackageManager.PackageInfoFlags.of(0), PackageManager.PackageInfoFlags.of(0),
) )
.versionName .versionName!!
} else { } else {
context.packageManager.getPackageInfo(context.packageName, 0).versionName context.packageManager.getPackageInfo(context.packageName, 0).versionName!!
} }
} }

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Trace( data class Trace(
@Json(name = "exposed") val exposed: Exposed, @Json(name = "exposed") val exposed: Exposed?,
@Json(name = "asset") val asset: NftAsset, @Json(name = "asset") val asset: NftAsset,
) )

View file

@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
data class ValidationResponse( data class ValidationResponse(
@Json(name = "status") val status: String, @Json(name = "status") val status: String,
@Json(name = "result_type") val resultType: String, @Json(name = "result_type") val resultType: String,
@Json(name = "description") val description: String?,
) )

View file

@ -24,7 +24,7 @@ sealed class ApiConfig {
Express, Express,
TangemTech, TangemTech,
StakeKit, StakeKit,
TangemVisa, TangemPay,
Attestation, Attestation,
BlockAid, BlockAid,
} }
@ -34,7 +34,7 @@ sealed class ApiConfig {
is Express -> ID.Express is Express -> ID.Express
is TangemTech -> ID.TangemTech is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit is StakeKit -> ID.StakeKit
is TangemVisa -> ID.TangemVisa is TangemPay -> ID.TangemPay
is Attestation -> ID.Attestation is Attestation -> ID.Attestation
is BlockAid -> ID.BlockAid is BlockAid -> ID.BlockAid
} }

View file

@ -3,7 +3,7 @@ package com.tangem.datasource.api.common.config
import com.tangem.utils.ProviderSuspend import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider import com.tangem.utils.version.AppVersionProvider
internal class TangemVisa( internal class TangemPay(
private val appVersionProvider: AppVersionProvider, private val appVersionProvider: AppVersionProvider,
) : ApiConfig() { ) : ApiConfig() {

View file

@ -1,27 +1,27 @@
package com.tangem.datasource.api.visa package com.tangem.datasource.api.pay
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest
import com.tangem.datasource.api.visa.models.request.ExchangeAccessTokenRequest import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardIdRequest import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.* import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query import retrofit2.http.Query
interface TangemVisaApi { interface TangemPayApi {
// region: auth // region: auth
@ -104,4 +104,7 @@ interface TangemVisaApi {
@Query("limit") limit: Int, @Query("limit") limit: Int,
@Query("offset") offset: Int, @Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse> ): ApiResponse<VisaTxHistoryResponse>
@GET("v1/customer/kyc")
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
} }

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
data class GenerateNoneByCardWalletRequest( data class GenerateNoneByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet", @Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "card_wallet_address") val cardWalletAddress: String, @Json(name = "card_wallet_address") val cardWalletAddress: String,
@Json(name = "card_id") val cardId: String,
) )

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -8,4 +8,5 @@ data class GetAccessTokenByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet", @Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "session_id") val sessionId: String, @Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String, @Json(name = "signature") val signature: String,
@Json(name = "salt") val salt: String,
) )

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.request package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class KycAccessInfoResponse(
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "token") val token: String,
@Json(name = "locale") val locale: String,
)
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.api.visa.models.response package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass

View file

@ -65,7 +65,7 @@ interface TangemTechApi {
suspend fun getQuotes( suspend fun getQuotes(
@Query("currencyId") currencyId: String, @Query("currencyId") currencyId: String,
@Query("coinIds") coinIds: String, @Query("coinIds") coinIds: String,
@Query("fields") fields: String = "price,priceChange24h,lastUpdatedAt", @Query("fields") fields: String,
): ApiResponse<QuotesResponse> ): ApiResponse<QuotesResponse>
@GET("promotion") @GET("promotion")
@ -176,13 +176,4 @@ interface TangemTechApi {
@GET("user-wallets/wallets/by-app/{app_id}") @GET("user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>> suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion // endregion
companion object {
val marketsQuoteFields = listOf(
"price",
"priceChange24h",
"priceChange1w",
"priceChange30d",
)
}
} }

View file

@ -53,7 +53,7 @@ internal object ApiConfigsModule {
@Provides @Provides
@IntoSet @IntoSet
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider)
@Provides @Provides
@IntoSet @IntoSet

View file

@ -17,7 +17,7 @@ import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApiV2 import com.tangem.datasource.api.tangemTech.TangemTechApiV2
import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.* import com.tangem.datasource.utils.*
@ -206,9 +206,9 @@ internal object NetworkModule {
analyticsErrorHandler: AnalyticsErrorHandler, analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager, apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore, appLogsStore: AppLogsStore,
): TangemVisaApi { ): TangemPayApi {
return createApi<TangemVisaApi>( return createApi<TangemPayApi>(
id = ApiConfig.ID.TangemVisa, id = ApiConfig.ID.TangemPay,
moshi = moshi, moshi = moshi,
context = context, context = context,
apiConfigsManager = apiConfigsManager, apiConfigsManager = apiConfigsManager,

View file

@ -1,41 +0,0 @@
package com.tangem.datasource.local.quote.converter
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
/**
* Converter from [QuotesResponse.Quote] to [Quote.Value]
*
* @property source status source
*
[REDACTED_AUTHOR]
*/
class QuoteConverter(
private val source: StatusSource,
) :
Converter<Map.Entry<String, QuotesResponse.Quote>, Quote.Value> {
/**
* Secondary constructor
*
* @param isCached flag that determines whether the quote is a cache
*/
constructor(isCached: Boolean) : this(
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
override fun convert(value: Map.Entry<String, QuotesResponse.Quote>): Quote.Value {
val (currencyId, quote) = value
return Quote.Value(
rawCurrencyId = CryptoCurrency.RawID(currencyId),
fiatRate = quote.price.orZero(),
priceChange = quote.priceChange24h.orZero().movePointLeft(2),
source = source,
)
}
}

View file

@ -37,7 +37,7 @@ private val API_CONFIGS = setOf(
Express(configManager, expressAuthProvider, appVersionProvider, appInfoProvider), Express(configManager, expressAuthProvider, appVersionProvider, appInfoProvider),
TangemTech(appVersionProvider, appAuthProvider, appInfoProvider), TangemTech(appVersionProvider, appAuthProvider, appInfoProvider),
StakeKit(stakeKitAuthProvider), StakeKit(stakeKitAuthProvider),
TangemVisa(appVersionProvider), TangemPay(appVersionProvider),
) )
/** /**
@ -88,7 +88,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
is Express -> createExpressModel() is Express -> createExpressModel()
is TangemTech -> createTangemTechModel() is TangemTech -> createTangemTechModel()
is StakeKit -> createStakeKitModel() is StakeKit -> createStakeKitModel()
is TangemVisa -> createVisaModel() is TangemPay -> createVisaModel()
is Attestation -> createAttestationModel() is Attestation -> createAttestationModel()
is BlockAid -> createBlockAidSdkModel() is BlockAid -> createBlockAidSdkModel()
} }
@ -182,7 +182,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
private fun createVisaModel(): Model { private fun createVisaModel(): Model {
return Model( return Model(
id = ApiConfig.ID.TangemVisa, id = ApiConfig.ID.TangemPay,
expected = ApiEnvironmentConfig( expected = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV, environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]", baseUrl = "[REDACTED_ENV_URL]",

View file

@ -1,13 +1,19 @@
plugins { plugins {
alias(deps.plugins.kotlin.jvm) alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt) alias(deps.plugins.kotlin.kapt)
id("configuration") id("configuration")
} }
android {
namespace = "com.tangem.core.decompose"
}
dependencies { dependencies {
api(projects.core.utils) api(projects.core.utils)
api(deps.decompose) api(deps.decompose)
api(deps.androidx.appCompat)
implementation(deps.kotlin.coroutines) implementation(deps.kotlin.coroutines)
implementation(deps.hilt.core) implementation(deps.hilt.core)

View file

@ -4,6 +4,7 @@ import com.arkivanov.decompose.ComponentContext
import com.tangem.core.decompose.di.HiltComponentBuilderOwner import com.tangem.core.decompose.di.HiltComponentBuilderOwner
import com.tangem.core.decompose.navigation.NavigationOwner import com.tangem.core.decompose.navigation.NavigationOwner
import com.tangem.core.decompose.ui.UiMessageSenderOwner import com.tangem.core.decompose.ui.UiMessageSenderOwner
import com.tangem.core.decompose.utils.ActivityHolder
import com.tangem.core.decompose.utils.ComponentScopeOwner import com.tangem.core.decompose.utils.ComponentScopeOwner
import com.tangem.core.decompose.utils.DispatchersOwner import com.tangem.core.decompose.utils.DispatchersOwner
import com.tangem.core.decompose.utils.TagsOwner import com.tangem.core.decompose.utils.TagsOwner
@ -20,4 +21,5 @@ interface AppComponentContext :
DispatchersOwner, DispatchersOwner,
UiMessageSenderOwner, UiMessageSenderOwner,
HiltComponentBuilderOwner, HiltComponentBuilderOwner,
TagsOwner TagsOwner,
ActivityHolder

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageHandler import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.decompose.ui.UiMessageSenderOwner import com.tangem.core.decompose.ui.UiMessageSenderOwner
import com.tangem.core.decompose.utils.ActivityHolder
import com.tangem.core.decompose.utils.ComponentCoroutineScope import com.tangem.core.decompose.utils.ComponentCoroutineScope
import com.tangem.core.decompose.utils.DispatchersOwner import com.tangem.core.decompose.utils.DispatchersOwner
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -58,7 +59,8 @@ fun AppComponentContext.childByContext(
NavigationOwner by this@childByContext, NavigationOwner by this@childByContext,
UiMessageSenderOwner by this@childByContext, UiMessageSenderOwner by this@childByContext,
DispatchersOwner by this@childByContext, DispatchersOwner by this@childByContext,
HiltComponentBuilderOwner by this@childByContext { HiltComponentBuilderOwner by this@childByContext,
ActivityHolder by this@childByContext {
override val tags: HashMap<String, Any> = HashMap() override val tags: HashMap<String, Any> = HashMap()

View file

@ -1,5 +1,6 @@
package com.tangem.core.decompose.context package com.tangem.core.decompose.context
import androidx.appcompat.app.AppCompatActivity
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.getOrCreate import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.di.ModelComponent
@ -17,6 +18,7 @@ class DefaultAppComponentContext(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: ModelComponent.Builder, override val hiltComponentBuilder: ModelComponent.Builder,
override val messageSender: UiMessageSender, override val messageSender: UiMessageSender,
override val activity: AppCompatActivity,
private val replaceRouter: Router? = null, private val replaceRouter: Router? = null,
) : AppComponentContext, ComponentContext by componentContext { ) : AppComponentContext, ComponentContext by componentContext {

View file

@ -0,0 +1,16 @@
package com.tangem.core.decompose.utils
import androidx.appcompat.app.AppCompatActivity
/**
* Interface for holding an [AppCompatActivity] instance.
*
* This interface is used to provide access to the activity in which the component is running.
*/
interface ActivityHolder {
/**
* The [AppCompatActivity] instance associated with this context.
*/
val activity: AppCompatActivity
}

View file

@ -1299,8 +1299,8 @@
<string name="warning_token_balance_not_updated">Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite.</string> <string name="warning_token_balance_not_updated">Der Kontostand ist möglicherweise veraltet. Aktualisiere bittre die Seite.</string>
<string name="warning_token_required_min_coin_reserve">Nicht genug %1$s. Lade Dein XLM-Konto auf, um dieses Token zu verknüpfen</string> <string name="warning_token_required_min_coin_reserve">Nicht genug %1$s. Lade Dein XLM-Konto auf, um dieses Token zu verknüpfen</string>
<string name="warning_token_trustline_button_title">Trustline aktivieren</string> <string name="warning_token_trustline_button_title">Trustline aktivieren</string>
<string name="warning_token_trustline_subtiile">Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s.</string> <string name="warning_token_trustline_subtitle">Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s.</string>
<string name="warning_token_trustline_tiile">Trustline erforderlich</string> <string name="warning_token_trustline_title">Trustline erforderlich</string>
<string name="wc_alert_audit_unknown_domain">Unbekannte Domäne</string> <string name="wc_alert_audit_unknown_domain">Unbekannte Domäne</string>
<string name="wc_alert_connect_anyway">Trotzdem verbinden</string> <string name="wc_alert_connect_anyway">Trotzdem verbinden</string>
<string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string> <string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string>

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