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

View file

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

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
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.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.MainPageScenario
import com.tangem.screens.DetailsPage
import com.tangem.screens.TopBarPage
import com.tangem.screens.WalletSettingsPage
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.DetailsTestScreen
import com.tangem.screens.TestTopBar
import com.tangem.screens.WalletSettingsTestScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test
@HiltAndroidTest
class DetailsScreenTest : BaseTestCase() {
@Test
fun walletWithoutBackupDetails() {
MainPageScenario.open()
step("Open wallet details") {
TopBarPage.moreButton.click()
fun walletWithoutBackupDetails() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
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
fun wallet2Details() {
MainPageScenario.open(ProductType.Wallet2)
step("Open wallet details") {
TopBarPage.moreButton.click()
fun wallet2Details() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
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
fun noteDetails() {
MainPageScenario.open(ProductType.Note)
step("Open wallet details") {
TopBarPage.moreButton.click()
fun noteDetails() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note))
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
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.screens.DisclaimerPage
import com.tangem.screens.MainPage
import com.tangem.screens.StoriesPage
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.MainTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test
@HiltAndroidTest
class ScanErrorTest : BaseTestCase() {
@Test
fun goToMain() {
step("Click on \"Accept\" button") {
DisclaimerPage.acceptButton.click()
fun goToMain() =
setupHooks().run {
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
import android.content.Intent.ACTION_VIEW
import androidx.test.espresso.intent.Intents
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 androidx.test.espresso.intent.matcher.UriMatchers
import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerPage
import com.tangem.screens.StoriesPage
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
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
@HiltAndroidTest
class StoriesTest : BaseTestCase() {
@Test
fun checkOrderButton() {
Intents.init()
step("Click Accept on ToS") {
DisclaimerPage.acceptButton.click()
fun clickOnOrderButton() =
setupHooks().run {
ComposeScreen.onComposeScreen<DisclaimerTestScreen>(composeTestRule) {
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:supportsRtl="true"
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
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.FeatureCoroutineExceptionHandler
import com.tangem.utils.extensions.uriValidate
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -353,7 +354,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
intentProcessor.removeAll()
// 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
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()
}
@ -556,5 +559,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
companion object {
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 com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
@ -21,11 +19,9 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor(
) : VisaAuthTokenStorage {
private val secureStorage by lazy {
AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_auth_storage",
),
AndroidSecureStorageV2(
appContext = applicationContext,
name = "visa_auth_storage",
)
}

View file

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

View file

@ -30,8 +30,10 @@ internal object RootAppComponentContextModule {
componentBuilder: ModelComponent.Builder,
@GlobalUiMessageSender messageSender: UiMessageSender,
): AppComponentContext {
val activity = context as AppCompatActivity
return DefaultAppComponentContext(
componentContext = (context as AppCompatActivity).defaultComponentContext(),
componentContext = activity.defaultComponentContext(),
activity = activity,
dispatchers = dispatchers,
hiltComponentBuilder = componentBuilder,
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.ManageTokensRepository
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.repositories.StakingRepository
import com.tangem.domain.tokens.TokensFeatureToggles
@ -36,8 +36,10 @@ internal object ManageTokensDomainModule {
@Provides
@Singleton
fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase {
return CreateCurrencyUseCase(customTokensRepository)
fun provideCreateCryptoCurrencyUseCase(
customTokensRepository: CustomTokensRepository,
): CreateCryptoCurrencyUseCase {
return CreateCryptoCurrencyUseCase(customTokensRepository)
}
@Provides
@ -71,7 +73,7 @@ internal object ManageTokensDomainModule {
derivationsRepository: DerivationsRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SaveManagedTokensUseCase {
@ -82,7 +84,7 @@ internal object ManageTokensDomainModule {
derivationsRepository = derivationsRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)

View file

@ -6,8 +6,8 @@ import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
@ -52,8 +52,8 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(singleQuoteSupplier: SingleQuoteSupplier): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(singleQuoteSupplier = singleQuoteSupplier)
fun provideGetTokenQuotesUseCase(singleQuoteStatusSupplier: SingleQuoteStatusSupplier): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier)
}
@Provides
@ -64,7 +64,7 @@ object MarketsDomainModule {
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): SaveMarketTokensUseCase {
@ -74,7 +74,7 @@ object MarketsDomainModule {
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
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.nft.*
import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.quotes.single.SingleQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -92,18 +92,18 @@ internal object NFTDomainModule {
@Singleton
fun provideGetNFTPriceUseCase(
nftRepository: NFTRepository,
singleQuoteSupplier: SingleQuoteSupplier,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
): GetNFTPriceUseCase {
return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier)
return GetNFTPriceUseCase(nftRepository, singleQuoteStatusSupplier)
}
@Provides
@Singleton
fun provideFetchNFTPriceUseCase(
nftRepository: NFTRepository,
singleQuoteFetcher: SingleQuoteFetcher,
singleQuoteStatusFetcher: SingleQuoteStatusFetcher,
): FetchNFTPriceUseCase {
return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher)
return FetchNFTPriceUseCase(nftRepository, singleQuoteStatusFetcher)
}
@Provides

View file

@ -1,12 +1,19 @@
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.api.SwapRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository
/**
[REDACTED_AUTHOR]
@ -17,7 +24,53 @@ internal object SwapDomainModule {
@Provides
@Singleton
fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase {
fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase {
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.SingleNetworkStatusSupplier
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepositoryV2
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
import com.tangem.domain.quotes.single.SingleQuoteSupplier
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
@ -42,7 +42,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): AddCryptoCurrenciesUseCase {
@ -50,7 +50,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
@ -62,7 +62,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchTokenListUseCase {
@ -70,7 +70,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
@ -161,7 +161,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchCurrencyStatusUseCase {
@ -169,7 +169,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
@ -181,7 +181,7 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
stakingRepository: StakingRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
tokensFeatureToggles: TokensFeatureToggles,
): FetchCardTokenListUseCase {
@ -189,7 +189,7 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
stakingRepository = stakingRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
)
@ -346,11 +346,11 @@ internal object TokensDomainModule {
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
multiQuoteFetcher: MultiQuoteFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
multiQuoteFetcher = multiQuoteFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
)
}
@ -368,27 +368,27 @@ internal object TokensDomainModule {
fun provideBaseCurrenciesStatusesOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleQuoteSupplier: SingleQuoteSupplier,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
): BaseCurrenciesStatusesOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleQuoteSupplier = singleQuoteSupplier,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
tokensFeatureToggles = tokensFeatureToggles,
@ -400,27 +400,27 @@ internal object TokensDomainModule {
fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepositoryV2: QuotesRepositoryV2,
quotesRepository: QuotesRepository,
stakingRepository: StakingRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteFetcher: MultiQuoteFetcher,
singleQuoteSupplier: SingleQuoteSupplier,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
quotesRepositoryV2 = quotesRepositoryV2,
quotesRepository = quotesRepository,
stakingRepository = stakingRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteFetcher = multiQuoteFetcher,
singleQuoteSupplier = singleQuoteSupplier,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
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
@Singleton
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.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager
@ -80,6 +81,10 @@ internal object UserWalletsListManagerModule {
context = applicationContext,
storageName = "user_wallets_storage",
),
androidSecureStorageV2 = AndroidSecureStorageV2(
appContext = applicationContext,
name = "user_wallets_storage2",
),
)
val authenticatedStorage = AuthenticatedStorage(

View file

@ -83,11 +83,14 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value)
.getOrElse {
Timber.i("Failed to get Access token for Wallet public key authorization")
return CompletionResult.Failure(it.tangemError)
}
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(
cardId = card.cardId,
// This is the wallet public key, not the address and it's alright, as the API expects it in this format
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(
publicKey = wallet.publicKey,
@ -96,15 +99,16 @@ internal class VisaCardScanHandler @Inject constructor(
return when (signChallengeResult) {
is CompletionResult.Success -> {
val signature = signChallengeResult.data.cardSignature ?: run {
Timber.i("Failed to sign challenge with Wallet public key")
return CompletionResult.Failure(VisaCardScanError.FailedToSignChallenge.tangemError)
}
val signature = signChallengeResult.data.walletSignature
val salt = signChallengeResult.data.salt
Timber.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()),
signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = signature.toHexString(),
salt = salt.toHexString(),
),
)
}
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.features.home.api.HomeComponent
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.HomeState
import com.tangem.tap.store
@ -57,12 +58,22 @@ internal class DefaultHomeComponent @AssistedInject constructor(
val activity = LocalContext.current.findActivity()
BackHandler(onBack = activity::finish)
SystemBarsIconsDisposable(darkIcons = false)
StoriesScreen(
homeState = homeState,
onScanButtonClick = model::onScanClick,
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
if (homeState.value.isV2StoriesEnabled) {
StoriesScreenV2(
homeState = homeState,
onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,
onAddExistingWalletButtonClick = model::onAddExistingWalletScreen,
onScanButtonClick = model::onScanClick,
)
} else {
StoriesScreen(
homeState = homeState,
onScanButtonClick = model::onScanClick,
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
}
ChangeRootBackgroundColorEffect(Color(color = 0xFF010101))
}

View file

@ -71,6 +71,14 @@ internal class HomeModel @Inject constructor(
.launchIn(modelScope)
}
fun onCreateNewWalletScreen() {
// TODO implement navigation to create new wallet
}
fun onAddExistingWalletScreen() {
// TODO implement navigation to add existing wallet
}
fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
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]
data class HomeState(
val scanInProgress: Boolean = false,
val isV2StoriesEnabled: Boolean = false,
val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(),
) : 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(deps.test.junit5)
implementation(deps.test.truth)
}

View file

@ -1,14 +1,22 @@
package com.tangem.common.test.data.quote
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.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 {
return QuoteConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first())
fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus {
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 {
return QuoteConverter(source = source).convert(value = mapOf(this).entries.first())
fun Pair<String, QuotesResponse.Quote>.toDomain(source: StatusSource = StatusSource.ACTUAL): QuoteStatus {
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.AmountStatePreviewData
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.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -40,18 +41,28 @@ fun AmountScreenContent(
bottom = TangemTheme.dimens.spacing16,
),
) {
amountField(
amountState = amountState,
isBalanceHidden = isBalanceHidden,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
)
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
selectedButton = amountState.selectedButton,
)
if (amountState.isRedesignEnabled) {
amountFieldV2(
amountState = amountState,
onValueChange = clickIntents::onAmountValueChange,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,
onCurrencyChange = clickIntents::onCurrencyChangeClick,
onMaxAmountClick = clickIntents::onMaxValueClick,
)
} else {
amountField(
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>
get() = sequenceOf(
AmountStatePreviewData.amountState,
AmountStatePreviewData.amountStateV2,
)
}
// endregion

View file

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

View file

@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.models
import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
@ -21,7 +22,7 @@ sealed class AmountState {
* @param selectedButton selected currency index
* @param isSegmentedButtonsEnabled indicates if currency switches is enabled
* @param amountTextField amount field state
* @param appCurrencyCode app currency code
* @param appCurrency app currency
* @param isEditingDisabled indicated whether amount is editable
* @param reduceAmountBy reduces amount to be sent by specified value
* @param isIgnoreReduce ignores reduce amount value
@ -30,15 +31,17 @@ sealed class AmountState {
override val isPrimaryButtonEnabled: Boolean,
val title: TextReference,
val availableBalance: TextReference,
val tokenName: TextReference,
val tokenIconState: CurrencyIconState,
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,
val selectedButton: Int,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: AmountFieldModel,
val appCurrencyCode: String,
val appCurrency: AppCurrency,
val isEditingDisabled: Boolean = false,
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
val isIgnoreReduce: Boolean = false,
val isRedesignEnabled: Boolean,
) : AmountState()
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.KeyboardOptions
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
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.wrappedList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import kotlinx.collections.immutable.persistentListOf
@ -15,6 +19,8 @@ import java.math.BigDecimal
object AmountStatePreviewData {
val emptyState = AmountState.Empty()
val amountState = AmountState.Data(
isPrimaryButtonEnabled = false,
title = stringReference("Family Wallet"),
@ -31,7 +37,8 @@ object AmountStatePreviewData {
isFiat = true,
),
),
appCurrencyCode = "usd",
appCurrency = AppCurrency.Default,
tokenName = stringReference("Tether"),
amountTextField = AmountFieldModel(
value = "",
onValueChange = {},
@ -60,6 +67,7 @@ object AmountStatePreviewData {
),
isSegmentedButtonsEnabled = true,
selectedButton = 0,
isRedesignEnabled = false,
)
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(
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 {
fiat(
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 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
internal fun AmountField(
amountField: AmountFieldModel,
@ -97,7 +47,7 @@ internal fun AmountField(
amountField.cryptoAmount to amountField.value
}
val requester = remember { FocusRequester() }
val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
@ -106,6 +56,7 @@ internal fun AmountField(
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
symbolColor = symbolColor,
),
onValueChange = onValueChange,
keyboardOptions = amountField.keyboardOptions,

View file

@ -2,20 +2,32 @@ package com.tangem.common.ui.amountScreen.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
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.Modifier
import androidx.compose.ui.draw.clip
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.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.CurrencyIconState
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
@ -64,10 +76,142 @@ internal fun LazyListScope.amountField(
)
AmountField(
amountField = amountState.amountTextField,
appCurrencyCode = amountState.appCurrencyCode,
appCurrencyCode = amountState.appCurrency.code,
onValueChange = onValueChange,
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)
}
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
val feeValue = value.multiply(rate)
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeValue,
fiatCurrencyCode = appCurrency.code,
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.domain.tokens.model.ScenarioUnavailabilityReason
@Suppress("CyclomaticComplexMethod")
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
return when (val unavailabilityReason = this) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
@ -46,6 +47,9 @@ fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
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 -> {
resourceReference(id = R.string.token_button_unavailability_reason_out_of_date_balance)
}

View file

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

View file

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

View file

@ -62,5 +62,17 @@
{
"name": "USEDESK_ENABLED",
"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,
PackageManager.PackageInfoFlags.of(0),
)
.versionName
.versionName!!
} 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)
data class Trace(
@Json(name = "exposed") val exposed: Exposed,
@Json(name = "exposed") val exposed: Exposed?,
@Json(name = "asset") val asset: NftAsset,
)

View file

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

View file

@ -24,7 +24,7 @@ sealed class ApiConfig {
Express,
TangemTech,
StakeKit,
TangemVisa,
TangemPay,
Attestation,
BlockAid,
}
@ -34,7 +34,7 @@ sealed class ApiConfig {
is Express -> ID.Express
is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit
is TangemVisa -> ID.TangemVisa
is TangemPay -> ID.TangemPay
is Attestation -> ID.Attestation
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.version.AppVersionProvider
internal class TangemVisa(
internal class TangemPay(
private val appVersionProvider: AppVersionProvider,
) : 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.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest
import com.tangem.datasource.api.visa.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.*
import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest
import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
interface TangemVisaApi {
interface TangemPayApi {
// region: auth
@ -104,4 +104,7 @@ interface TangemVisaApi {
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): 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.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.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.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.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.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.JsonClass
@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
data class GenerateNoneByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@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.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.JsonClass
@ -8,4 +8,5 @@ data class GetAccessTokenByCardWalletRequest(
@Json(name = "auth_type") val authType: String = "card_wallet",
@Json(name = "session_id") val sessionId: 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.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.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.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.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.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.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.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.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.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.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.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.JsonClass

View file

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

View file

@ -53,7 +53,7 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider)
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider)
@Provides
@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.tangemTech.TangemTechApi
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.preferences.AppPreferencesStore
import com.tangem.datasource.utils.*
@ -206,9 +206,9 @@ internal object NetworkModule {
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemVisaApi {
return createApi<TangemVisaApi>(
id = ApiConfig.ID.TangemVisa,
): TangemPayApi {
return createApi<TangemPayApi>(
id = ApiConfig.ID.TangemPay,
moshi = moshi,
context = context,
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),
TangemTech(appVersionProvider, appAuthProvider, appInfoProvider),
StakeKit(stakeKitAuthProvider),
TangemVisa(appVersionProvider),
TangemPay(appVersionProvider),
)
/**
@ -88,7 +88,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
is Express -> createExpressModel()
is TangemTech -> createTangemTechModel()
is StakeKit -> createStakeKitModel()
is TangemVisa -> createVisaModel()
is TangemPay -> createVisaModel()
is Attestation -> createAttestationModel()
is BlockAid -> createBlockAidSdkModel()
}
@ -182,7 +182,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
private fun createVisaModel(): Model {
return Model(
id = ApiConfig.ID.TangemVisa,
id = ApiConfig.ID.TangemPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",

View file

@ -1,13 +1,19 @@
plugins {
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.core.decompose"
}
dependencies {
api(projects.core.utils)
api(deps.decompose)
api(deps.androidx.appCompat)
implementation(deps.kotlin.coroutines)
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.navigation.NavigationOwner
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.DispatchersOwner
import com.tangem.core.decompose.utils.TagsOwner
@ -20,4 +21,5 @@ interface AppComponentContext :
DispatchersOwner,
UiMessageSenderOwner,
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.UiMessageSender
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.DispatchersOwner
import kotlinx.coroutines.CoroutineScope
@ -58,7 +59,8 @@ fun AppComponentContext.childByContext(
NavigationOwner by this@childByContext,
UiMessageSenderOwner 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()

View file

@ -1,5 +1,6 @@
package com.tangem.core.decompose.context
import androidx.appcompat.app.AppCompatActivity
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.tangem.core.decompose.di.ModelComponent
@ -17,6 +18,7 @@ class DefaultAppComponentContext(
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: ModelComponent.Builder,
override val messageSender: UiMessageSender,
override val activity: AppCompatActivity,
private val replaceRouter: Router? = null,
) : 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_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_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_tiile">Trustline erforderlich</string>
<string name="warning_token_trustline_subtitle">Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s.</string>
<string name="warning_token_trustline_title">Trustline erforderlich</string>
<string name="wc_alert_audit_unknown_domain">Unbekannte Domäne</string>
<string name="wc_alert_connect_anyway">Trotzdem verbinden</string>
<string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string>

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