Updated on 2026-08-14
This commit is contained in:
commit
51eeaead1b
56 changed files with 852 additions and 252 deletions
|
|
@ -188,6 +188,7 @@ abstract class BaseTestCase : TestCase(
|
|||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||
"GASLESS_APPROVAL_ENABLED" to true,
|
||||
"MAIN_SCREEN_QR_SCANNING_ENABLED" to true,
|
||||
"ADD_AND_MANAGE_TOKENS_ENABLED" to true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,6 @@ fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) {
|
|||
runCatching { onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Assert 'Organize tokens' button is enabled") {
|
||||
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
|
||||
runCatching { onMainScreen { organizeTokensButton().assertIsEnabled() } }.isSuccess
|
||||
}
|
||||
}
|
||||
step("Wait for all wallet managers to initialize") {
|
||||
awaitWalletManagersStabilized()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ fun BaseTestCase.checkSingleCurrencyMainScreen(
|
|||
onMainScreen { emptyTransactionBlockExploreButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonWithoutLazySearch.assertIsNotDisplayed() }
|
||||
step("Assert 'Add & Manage' button is not displayed") {
|
||||
onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,8 +112,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
|
|||
step("Assert 'Receive' button is not displayed") {
|
||||
onMainScreen { receiveButton.assertIsNotDisplayed() }
|
||||
}
|
||||
step("Assert 'Organize tokens' button is displayed") {
|
||||
onMainScreen { organizeTokensButton().assertIsDisplayed() }
|
||||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButton().assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.scenarios
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.SwipeDirection
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.swipeVertical
|
||||
import com.tangem.screens.onAddAndManageBottomSheet
|
||||
import com.tangem.screens.onMainScreen
|
||||
import io.qameta.allure.kotlin.Allure.step
|
||||
|
||||
fun BaseTestCase.openOrganizeTokensScreen() {
|
||||
step("Swipe to 'Add & Manage' button") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click on 'Add & Manage' button") {
|
||||
onMainScreen { addAndManageButton().clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Organize tokens' button in bottom sheet") {
|
||||
onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.KNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
class AddAndManageBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<AddAndManageBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val addTokensButton: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.add_and_manage_sheet_manage_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val organizeTokensButton: KNode = child {
|
||||
hasText(getResourceString(CoreResR.string.add_and_manage_sheet_organize_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onAddAndManageBottomSheet(function: AddAndManageBottomSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -19,6 +19,7 @@ import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
|
|||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
|
|
@ -214,8 +215,8 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied))
|
||||
}
|
||||
|
||||
val organizeTokensButtonNode: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
|
||||
val addAndManageButtonNode: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
@ -265,18 +266,18 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun organizeTokensButton(): KNode {
|
||||
fun addAndManageButton(): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
|
||||
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
||||
}.child<KNode> {
|
||||
hasText(getResourceString(R.string.organize_tokens_title))
|
||||
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
val organizeTokensButtonWithoutLazySearch: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON)
|
||||
hasText(getResourceString(R.string.organize_tokens_title))
|
||||
val addAndManageButtonWithoutLazySearch: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON)
|
||||
hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ package com.tangem.tests
|
|||
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.SwipeDirection
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.swipeVertical
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.openOrganizeTokensScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.onMainScreen
|
||||
import com.tangem.screens.onOrganizeTokensScreen
|
||||
|
|
@ -31,12 +30,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Swipe to 'Organize tokens' button") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click 'Organize tokens' button") {
|
||||
onMainScreen { organizeTokensButton().clickWithAssertion() }
|
||||
step("Open 'Organize tokens' screen") {
|
||||
openOrganizeTokensScreen()
|
||||
}
|
||||
step("Assert 'Organize tokens' screen is opened") {
|
||||
onOrganizeTokensScreen {
|
||||
|
|
@ -56,12 +51,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
step("Assert tokens were grouped on 'Main screen'") {
|
||||
onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() }
|
||||
}
|
||||
step("Swipe to 'Organize tokens' button") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click 'Organize tokens' button") {
|
||||
onMainScreen { organizeTokensButton().clickWithAssertion() }
|
||||
step("Open 'Organize tokens' screen") {
|
||||
openOrganizeTokensScreen()
|
||||
}
|
||||
step("Assert 'Organize tokens' screen is opened") {
|
||||
onOrganizeTokensScreen {
|
||||
|
|
@ -104,12 +95,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Swipe to 'Organize tokens' button") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click 'Organize tokens' button") {
|
||||
onMainScreen { organizeTokensButton().clickWithAssertion() }
|
||||
step("Open 'Organize tokens' screen") {
|
||||
openOrganizeTokensScreen()
|
||||
}
|
||||
step("Check positions of tokens on 'Organize tokens' screen") {
|
||||
onOrganizeTokensScreen {
|
||||
|
|
@ -141,12 +128,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Swipe to 'Organize tokens' button") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click 'Organize tokens' button") {
|
||||
onMainScreen { organizeTokensButton().clickWithAssertion() }
|
||||
step("Open 'Organize tokens' screen") {
|
||||
openOrganizeTokensScreen()
|
||||
}
|
||||
step("Drag $bitcoinTitle down on 'Organize tokens' screen") {
|
||||
composeTestRule.waitUntil(timeoutMillis = 100_000) {
|
||||
|
|
@ -191,12 +174,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Swipe to 'Organize tokens' button") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
}
|
||||
step("Click 'Organize tokens' button") {
|
||||
onMainScreen { organizeTokensButton().clickWithAssertion() }
|
||||
step("Open 'Organize tokens' screen") {
|
||||
openOrganizeTokensScreen()
|
||||
}
|
||||
step("Check positions of tokens on 'Organize tokens' screen") {
|
||||
onOrganizeTokensScreen {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ class StakingTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is displayed") {
|
||||
onMainScreen { organizeTokensButton().assertIsDisplayed() }
|
||||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButton().assertIsDisplayed() }
|
||||
}
|
||||
step("Swipe up") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
|
|
@ -98,8 +98,8 @@ class StakingTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is displayed") {
|
||||
onMainScreen { organizeTokensButton().assertIsDisplayed() }
|
||||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButton().assertIsDisplayed() }
|
||||
}
|
||||
step("Swipe up") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
|
|
@ -156,8 +156,8 @@ class StakingTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is displayed") {
|
||||
onMainScreen { organizeTokensButton().assertIsDisplayed() }
|
||||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButton().assertIsDisplayed() }
|
||||
}
|
||||
step("Swipe up") {
|
||||
swipeVertical(SwipeDirection.UP)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ class MainScreenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is displayed") {
|
||||
onMainScreen { organizeTokensButton().assertIsDisplayed() }
|
||||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButton().assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,8 +56,8 @@ class MainScreenTest : BaseTestCase() {
|
|||
step("Synchronize addresses") {
|
||||
synchronizeAddresses()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()}
|
||||
step("Assert 'Add & Manage' button is not displayed") {
|
||||
onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,8 +81,8 @@ class MainScreenTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()}
|
||||
step("Assert 'Add & Manage' button is not displayed") {
|
||||
onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,8 +106,8 @@ class MainScreenTest : BaseTestCase() {
|
|||
step("Open 'Main Screen'") {
|
||||
openMainScreen()
|
||||
}
|
||||
step("Assert 'Organize tokens' button is not displayed") {
|
||||
onMainScreen { organizeTokensButtonNode.assertIsDisplayed()}
|
||||
step("Assert 'Add & Manage' button is displayed") {
|
||||
onMainScreen { addAndManageButtonNode.assertIsDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ADD_AND_MANAGE_TOKENS_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.38"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_BITCOIN_ENABLED",
|
||||
|
|
|
|||
|
|
@ -1714,6 +1714,7 @@
|
|||
<string name="tangempay_card_details_withdraw_error_title">Withdraw unavailable now</string>
|
||||
<string name="tangempay_card_details_withdraw_in_progress_description">You can\'t initiate swap or new withdrawal till the current one is finished</string>
|
||||
<string name="tangempay_card_details_withdraw_in_progress_title">Withdrawal in progress</string>
|
||||
<string name="tangempay_card_edit_name_placeholder">Card name</string>
|
||||
<string name="tangempay_card_limit_setup_amount_subtitle">Set a limit from %s</string>
|
||||
<string name="tangempay_card_limit_setup_error_message">We couldn’t set the limit. Please try again</string>
|
||||
<string name="tangempay_card_page_daily_limit_change">Change</string>
|
||||
|
|
|
|||
|
|
@ -98,13 +98,22 @@ fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleRefere
|
|||
}
|
||||
|
||||
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||
val formattedAmount = formatter.format(formattingAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
val currencySymbol = formatterCurrency.getSymbol(locale)
|
||||
val rawFormatted = formatter.format(formattingAmount)
|
||||
|
||||
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||
val separatorIndex = decimalSeparator?.let { rawFormatted.indexOf(it).takeIf { i -> i >= 0 } }
|
||||
?: rawFormatted.length
|
||||
|
||||
val wholePart = formattedAmount.take(separatorIndex)
|
||||
val fractionalPart = formattedAmount.drop(separatorIndex)
|
||||
val formattedAmount = rawFormatted.replace(currencySymbol, fiatCurrencySymbol)
|
||||
val offset = fiatCurrencySymbol.length - currencySymbol.length
|
||||
val adjustedIndex = if (rawFormatted.indexOf(currencySymbol) in 0 until separatorIndex) {
|
||||
separatorIndex + offset
|
||||
} else {
|
||||
separatorIndex
|
||||
}
|
||||
|
||||
val wholePart = formattedAmount.take(adjustedIndex)
|
||||
val fractionalPart = formattedAmount.drop(adjustedIndex)
|
||||
|
||||
combinedReference(
|
||||
if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY,
|
||||
|
|
@ -192,13 +201,22 @@ private fun BigDecimalFiatFormatStyled.price(spanStyleReference: SpanStyleRefere
|
|||
}
|
||||
|
||||
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
|
||||
val formattedAmount = formatter.format(priceAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
val currencySymbol = formatterCurrency.getSymbol(locale)
|
||||
val rawFormatted = formatter.format(priceAmount)
|
||||
|
||||
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
|
||||
val separatorIndex = decimalSeparator?.let { rawFormatted.indexOf(it).takeIf { i -> i >= 0 } }
|
||||
?: rawFormatted.length
|
||||
|
||||
val wholePart = formattedAmount.take(separatorIndex)
|
||||
val fractionalPart = formattedAmount.drop(separatorIndex)
|
||||
val formattedAmount = rawFormatted.replace(currencySymbol, fiatCurrencySymbol)
|
||||
val offset = fiatCurrencySymbol.length - currencySymbol.length
|
||||
val adjustedIndex = if (rawFormatted.indexOf(currencySymbol) in 0 until separatorIndex) {
|
||||
separatorIndex + offset
|
||||
} else {
|
||||
separatorIndex
|
||||
}
|
||||
|
||||
val wholePart = formattedAmount.take(adjustedIndex)
|
||||
val fractionalPart = formattedAmount.drop(adjustedIndex)
|
||||
|
||||
combinedReference(
|
||||
stringReference(wholePart),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ object MainScreenTestTags {
|
|||
const val TOKEN_LIST_ITEM = "MAIN_SCREEN_TOKEN_LIST_ITEM"
|
||||
const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM"
|
||||
const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON"
|
||||
const val ADD_AND_MANAGE_BUTTON = "MAIN_SCREEN_ADD_AND_MANAGE_BUTTON"
|
||||
const val CARD_TITLE = "MAIN_SCREEN_CARD_TITLE"
|
||||
const val CARD_IMAGE = "MAIN_SCREEN_CARD_IMAGE"
|
||||
const val DEVICES_COUNT = "MAIN_SCREEN_DEVICES_COUNT"
|
||||
|
|
|
|||
|
|
@ -294,4 +294,52 @@ internal class BigDecimalFiatFormatTest {
|
|||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.00000000000000000000123".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price zero is formatted with default precision and does not crash`() {
|
||||
val testValue = BigDecimal.ZERO
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("0.00".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price negative integer keeps sign and does not crash`() {
|
||||
val testValue = BigDecimal("-500")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("-" + "500.00".addUsdSymbolLeft())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `price negative fractional keeps sign and does not crash`() {
|
||||
val testValue = BigDecimal("-0.5")
|
||||
|
||||
val formatted = testValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = usdCurrencyCode,
|
||||
fiatCurrencySymbol = usdSymbol,
|
||||
locale = testLocale,
|
||||
).price()
|
||||
}
|
||||
|
||||
Truth.assertThat(formatted)
|
||||
.isEqualTo("-" + "0.50".addUsdSymbolLeft())
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import com.tangem.domain.pay.repository.*
|
|||
import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
|
||||
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
|
|
@ -148,6 +149,14 @@ internal interface TangemPayDataModule {
|
|||
return SetTangemPayCardLimitUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUpdateTangemPayCardNameUseCase(
|
||||
cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): UpdateTangemPayCardNameUseCase {
|
||||
return UpdateTangemPayCardNameUseCase(cardDetailsRepository, paymentAccountStatusFetcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTangemPayCryptoCurrencyStatusUseCase(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
package com.tangem.domain.pay.flow
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface PaymentAccountStatusFetcher : FlowFetcher<PaymentAccountStatusFetcher.Params> {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
|
||||
return invoke(Params(userWalletId))
|
||||
}
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -17,9 +17,6 @@ class SetTangemPayCardLimitUseCase(
|
|||
amount: BigDecimal,
|
||||
): Either<UniversalError, Unit> {
|
||||
return cardDetailsRepository.updateCardLimit(cardId, userWalletId, amount.toPlainString())
|
||||
.onRight {
|
||||
val params = PaymentAccountStatusFetcher.Params(userWalletId)
|
||||
paymentAccountStatusFetcher.invoke(params)
|
||||
}
|
||||
.onRight { paymentAccountStatusFetcher.invoke(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
|
||||
class UpdateTangemPayCardNameUseCase(
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
cardId: String,
|
||||
userWalletId: UserWalletId,
|
||||
displayName: CardDisplayName,
|
||||
): Either<UniversalError, Unit> {
|
||||
return cardDetailsRepository.updateCardDisplayName(cardId, userWalletId, displayName)
|
||||
.onRight { paymentAccountStatusFetcher(userWalletId) }
|
||||
}
|
||||
}
|
||||
|
|
@ -217,4 +217,29 @@ sealed class TangemPayAnalyticsEvents(
|
|||
categoryName = "Visa Onboarding",
|
||||
event = "Visa Permanent Button Clicked",
|
||||
)
|
||||
|
||||
class CardIconClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Card Icon Clicked",
|
||||
)
|
||||
|
||||
class CardManagementScreenOpened : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Card Management Screen Opened",
|
||||
)
|
||||
|
||||
class AddExtraCardClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Add Extra Card Clicked",
|
||||
)
|
||||
|
||||
class FakeDoorPopupDisplayed : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Fakedoor Popup Displayed",
|
||||
)
|
||||
|
||||
class FakeDoorGotitClicked : TangemPayAnalyticsEvents(
|
||||
categoryName = "Visa Card Management",
|
||||
event = "Visa Fakedoor Gotit Clicked",
|
||||
)
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ interface FeedEntryComponent : ComposableContentComponent {
|
|||
fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
onExpandSheet: () -> Unit,
|
||||
modifier: Modifier,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
override fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
onExpandSheet: () -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val bsState by bottomSheetState
|
||||
|
|
@ -191,6 +192,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
bottomSheetState = bottomSheetState,
|
||||
stackState = stackStack,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
onExpandSheet = onExpandSheet,
|
||||
isOpenedInBottomSheet = true,
|
||||
)
|
||||
}
|
||||
|
|
@ -213,6 +215,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
bottomSheetState = bottomSheetState,
|
||||
stackState = stack.subscribeAsState(),
|
||||
onHeaderSizeChange = {},
|
||||
onExpandSheet = {},
|
||||
isOpenedInBottomSheet = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ internal class MarketsTokenItemConverter(
|
|||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
|
|
@ -9,6 +10,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
|
|
@ -34,6 +36,7 @@ internal fun EntryContent(
|
|||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
onExpandSheet: () -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
if (LocalRedesignEnabled.current) {
|
||||
|
|
@ -41,6 +44,7 @@ internal fun EntryContent(
|
|||
bottomSheetState = bottomSheetState,
|
||||
stackState = stackState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
onExpandSheet = onExpandSheet,
|
||||
isOpenedInBottomSheet = isOpenedInBottomSheet,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -48,6 +52,7 @@ internal fun EntryContent(
|
|||
bottomSheetState = bottomSheetState,
|
||||
stackState = stackState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
onExpandSheet = onExpandSheet,
|
||||
isOpenedInBottomSheet = isOpenedInBottomSheet,
|
||||
)
|
||||
}
|
||||
|
|
@ -58,6 +63,7 @@ private fun EntryContentV1(
|
|||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
onExpandSheet: () -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
|
@ -69,7 +75,7 @@ private fun EntryContentV1(
|
|||
containerColor = background,
|
||||
contentWindowInsets = WindowInsetsZero,
|
||||
topBar = {
|
||||
Children(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (!isOpenedInBottomSheet) {
|
||||
|
|
@ -85,10 +91,17 @@ private fun EntryContentV1(
|
|||
}
|
||||
}
|
||||
},
|
||||
stack = stackState.value,
|
||||
animation = stackAnimation,
|
||||
) { child ->
|
||||
child.instance.Title(bottomSheetState)
|
||||
) {
|
||||
Children(
|
||||
stack = stackState.value,
|
||||
animation = stackAnimation,
|
||||
) { child ->
|
||||
child.instance.Title(bottomSheetState)
|
||||
}
|
||||
CollapsedTitleClickOverlay(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onExpandSheet = onExpandSheet,
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
|
|
@ -112,6 +125,7 @@ private fun EntryContentV2(
|
|||
bottomSheetState: State<BottomSheetState>,
|
||||
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
onExpandSheet: () -> Unit,
|
||||
isOpenedInBottomSheet: Boolean,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
|
@ -145,7 +159,7 @@ private fun EntryContentV2(
|
|||
bottomSheetState = bottomSheetState,
|
||||
)
|
||||
}
|
||||
AnimatedContent(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.then(
|
||||
|
|
@ -164,14 +178,37 @@ private fun EntryContentV2(
|
|||
}
|
||||
}
|
||||
},
|
||||
targetState = stackState.value.active,
|
||||
transitionSpec = animationAppBar,
|
||||
contentKey = { it.key },
|
||||
label = "FeedEntryAppBar",
|
||||
) { state ->
|
||||
state.instance.Title(bottomSheetState)
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = stackState.value.active,
|
||||
transitionSpec = animationAppBar,
|
||||
contentKey = { it.key },
|
||||
label = "FeedEntryAppBar",
|
||||
) { state ->
|
||||
state.instance.Title(bottomSheetState)
|
||||
}
|
||||
CollapsedTitleClickOverlay(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onExpandSheet = onExpandSheet,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.CollapsedTitleClickOverlay(bottomSheetState: State<BottomSheetState>, onExpandSheet: () -> Unit) {
|
||||
if (bottomSheetState.value == BottomSheetState.COLLAPSED) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onClick = onExpandSheet,
|
||||
)
|
||||
.clearAndSetSemantics {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -225,8 +225,7 @@ private fun TransactionCardLoading(modifier: Modifier = Modifier) {
|
|||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
TextShimmer(
|
||||
|
|
@ -278,7 +277,7 @@ private fun TransactionCardLoading(modifier: Modifier = Modifier) {
|
|||
|
||||
@Composable
|
||||
private fun Header(type: TransactionCardType, balance: String, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
|
|
@ -288,8 +287,6 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
end = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.testTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val titleColor = if (type.inputError is TransactionCardType.InputError.Empty) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
|
|
@ -310,9 +307,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
|
|||
text = balanceText,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.testTag(SwapTokenScreenTestTags.BALANCE),
|
||||
modifier = Modifier.testTag(SwapTokenScreenTestTags.BALANCE),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,10 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
context = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayCardPageComponent.Params(initialStatus = params.initialStatus),
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = params,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ internal class TangemPayAddToWalletComponent(
|
|||
params = TangemPayCardDetailsBlockComponent.Params(
|
||||
card = params.initialStatus.firstCard(),
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
isDisplayCardNameEnabled = false,
|
||||
isEditingNameEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal class TangemPayCardPageScreenComponent(
|
|||
params = TangemPayCardDetailsBlockComponent.Params(
|
||||
card = params.initialStatus.firstCard(),
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
isDisplayCardNameEnabled = true,
|
||||
isEditingNameEnabled = true,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ internal class TangemPayEditDisplayNameComponent(
|
|||
params = TangemPayCardDetailsBlockComponent.Params(
|
||||
card = params.initialStatus.firstCard(),
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
isDisplayCardNameEnabled = true,
|
||||
isEditingNameEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ internal class TangemPayEditDisplayNameComponent(
|
|||
val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle()
|
||||
val editingCardDetailsState = cardDetailsState.copy(
|
||||
displayNameState = DisplayNameState.Editing(
|
||||
displayName = state.editingValue,
|
||||
displayName = state.editingValue.text,
|
||||
editingValue = state.editingValue,
|
||||
onValueChanged = state.onValueChanged,
|
||||
onSubmit = state.onDoneClick,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ internal interface TangemPayCardDetailsBlockComponent {
|
|||
data class Params(
|
||||
val card: TangemPayCard,
|
||||
val userWalletId: UserWalletId,
|
||||
val isDisplayCardNameEnabled: Boolean,
|
||||
val isEditingNameEnabled: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
|
|
@ -8,21 +9,33 @@ import com.tangem.utils.StringsSigns
|
|||
|
||||
internal class TangemPayCardDetailsBlockStateFactory(
|
||||
private val cardNumberEnd: String,
|
||||
private val displayNameState: DisplayNameState?,
|
||||
private val displayName: CardDisplayName?,
|
||||
private val isEditingNameEnabled: Boolean,
|
||||
private val onEditNameClick: () -> Unit,
|
||||
private val onReveal: () -> Unit,
|
||||
private val onCopy: (String, CardDataType) -> Unit,
|
||||
) {
|
||||
|
||||
fun getInitialState() = TangemPayCardDetailsUM(
|
||||
number = "",
|
||||
numberShort = "${StringsSigns.ASTERISK}$cardNumberEnd",
|
||||
expiry = "",
|
||||
cvv = "",
|
||||
buttonText = resourceReference(R.string.tangempay_card_details_reveal_text),
|
||||
onClick = onReveal,
|
||||
onCopy = onCopy,
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = displayNameState,
|
||||
)
|
||||
fun getInitialState(): TangemPayCardDetailsUM {
|
||||
return TangemPayCardDetailsUM(
|
||||
number = "",
|
||||
numberShort = "${StringsSigns.ASTERISK}$cardNumberEnd",
|
||||
expiry = "",
|
||||
cvv = "",
|
||||
buttonText = resourceReference(R.string.tangempay_card_details_reveal_text),
|
||||
onClick = onReveal,
|
||||
onCopy = onCopy,
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = if (displayName != null) {
|
||||
DisplayNameState.Display(
|
||||
displayName = displayName.value,
|
||||
onClick = onEditNameClick,
|
||||
isEditingEnabled = isEditingNameEnabled,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +59,7 @@ internal class TangemPayDetailsStateFactory(
|
|||
),
|
||||
isBalanceHidden = false,
|
||||
addFundsEnabled = true,
|
||||
addToWalletBlockState = null,
|
||||
accountDeactivatedNotificationConfig = NotificationConfig(
|
||||
title = resourceReference(R.string.tangempay_account_deactivated_message_title),
|
||||
subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
|
|
@ -12,6 +13,7 @@ internal data class TangemPayDetailsUM(
|
|||
val topBarConfig: TangemPayDetailsTopBarConfig,
|
||||
val pullToRefreshConfig: PullToRefreshConfig,
|
||||
val balanceBlockState: TangemPayDetailsBalanceBlockState,
|
||||
val addToWalletBlockState: AddToWalletBlockState?,
|
||||
val isBalanceHidden: Boolean,
|
||||
val addFundsEnabled: Boolean,
|
||||
val accountDeactivatedNotificationConfig: NotificationConfig?,
|
||||
|
|
@ -39,15 +41,23 @@ internal sealed interface DisplayNameState {
|
|||
data class Display(
|
||||
override val displayName: String,
|
||||
val onClick: () -> Unit,
|
||||
val isEditingEnabled: Boolean,
|
||||
) : DisplayNameState
|
||||
|
||||
data class Editing(
|
||||
override val displayName: String,
|
||||
val editingValue: String,
|
||||
val onValueChanged: (String) -> Unit,
|
||||
val editingValue: TextFieldValue,
|
||||
val onValueChanged: (TextFieldValue) -> Unit,
|
||||
val onSubmit: () -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
) : DisplayNameState
|
||||
|
||||
fun copySealed(displayName: String): DisplayNameState {
|
||||
return when (this) {
|
||||
is Display -> copy(displayName = displayName)
|
||||
is Editing -> copy(displayName = displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TangemPayDetailsBalanceBlockState {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.features.tangempay.entity
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
|
||||
internal data class TangemPayEditDisplayNameUM(
|
||||
val editingValue: String,
|
||||
val editingValue: TextFieldValue,
|
||||
val isLoading: Boolean,
|
||||
val onValueChanged: (String) -> Unit,
|
||||
val onValueChanged: (TextFieldValue) -> Unit,
|
||||
val onDoneClick: () -> Unit,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
|
@ -117,7 +117,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onAmountChange(newValue: String) {
|
||||
if (newValue.toBigDecimalOrNull() == null) return
|
||||
if (newValue.isNotEmpty() && newValue.toBigDecimalOrNull() == null) return
|
||||
uiState.update { state ->
|
||||
state.copy(
|
||||
amountFieldModel = state.amountFieldModel.copy(value = newValue),
|
||||
|
|
|
|||
|
|
@ -10,11 +10,16 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.hasCardWithId
|
||||
import com.tangem.domain.models.account.requireCardWithId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.DisplayNameState
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
|
||||
|
|
@ -22,10 +27,12 @@ import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
|
|||
import com.tangem.features.tangempay.model.transformers.DetailsHiddenStateTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.DetailsRevealProgressStateTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.DetailsRevealedStateTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayCardDetailsUpdateNameTransformer
|
||||
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -46,6 +53,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
private val cardDetailsEventListener: CardDetailsEventListener,
|
||||
private val analytics: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require()
|
||||
|
|
@ -53,12 +61,9 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
|
||||
private val stateFactory = TangemPayCardDetailsBlockStateFactory(
|
||||
cardNumberEnd = card.lastDigits,
|
||||
displayNameState = card.displayName?.takeIf { params.isDisplayCardNameEnabled }?.let { displayName ->
|
||||
DisplayNameState.Display(
|
||||
displayName = displayName.value,
|
||||
onClick = ::startEditingDisplayName,
|
||||
)
|
||||
},
|
||||
displayName = card.displayName,
|
||||
isEditingNameEnabled = params.isEditingNameEnabled,
|
||||
onEditNameClick = ::startEditingDisplayName,
|
||||
onReveal = ::revealCardDetails,
|
||||
onCopy = ::copyData,
|
||||
)
|
||||
|
|
@ -70,6 +75,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
private val showCardDetailsTimerJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
subscribeToCardNameChanges(cardId = card.id, userWalletId = params.userWalletId)
|
||||
subscribeToCardFrozenState()
|
||||
modelScope.launch {
|
||||
cardDetailsEventListener.event.collectLatest { event ->
|
||||
|
|
@ -81,6 +87,23 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) {
|
||||
paymentAccountStatusSupplier.invoke(userWalletId)
|
||||
.onEach { state ->
|
||||
val status = state.value
|
||||
if (status is PaymentAccountStatusValue.Loaded &&
|
||||
status.source == StatusSource.ACTUAL &&
|
||||
status.hasCardWithId(cardId)
|
||||
) {
|
||||
val card = status.requireCardWithId(cardId)
|
||||
val displayName = card.displayName ?: return@onEach
|
||||
|
||||
uiState.update(TangemPayCardDetailsUpdateNameTransformer(displayName))
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToCardFrozenState() {
|
||||
cardDetailsRepository
|
||||
.cardFrozenState(card.id)
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ internal class TangemPayCardPageModel @Inject constructor(
|
|||
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed
|
||||
init {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardManagementScreenOpened())
|
||||
fetchAddToWalletBanner()
|
||||
|
||||
paymentAccountStatusSupplier.invoke(userWalletId)
|
||||
|
|
|
|||
|
|
@ -38,10 +38,7 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
|
|||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
|
||||
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
|
||||
import com.tangem.features.tangempay.model.transformers.DetailBalanceVisibilityTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.DetailsBalanceTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayFreezeUnfreezeStateTransformer
|
||||
import com.tangem.features.tangempay.model.transformers.*
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.utils.*
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsEvent
|
||||
|
|
@ -110,6 +107,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
private val fetchBalanceJobHolder = JobHolder()
|
||||
private val addToWalletBannerJobHolder = JobHolder()
|
||||
|
||||
private var balance: TangemPayCardBalance? = null
|
||||
|
||||
|
|
@ -121,6 +119,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
fetchBalance()
|
||||
if (!isTangemPayDeactivated && firstCard != null) {
|
||||
subscribeToCardFrozenState(firstCard.id)
|
||||
fetchAddToWalletBanner()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,6 +216,24 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}.saveIn(fetchBalanceJobHolder)
|
||||
}
|
||||
|
||||
private fun fetchAddToWalletBanner() {
|
||||
modelScope.launch {
|
||||
val isDone = try {
|
||||
cardDetailsRepository.isAddToWalletDone(userWalletId).getOrNull() == true
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Error", e)
|
||||
return@launch
|
||||
}
|
||||
uiState.update(
|
||||
transformer = DetailsAddToWalletBannerTransformer(
|
||||
onClickBanner = ::onClickAddToWalletBlock,
|
||||
onClickCloseBanner = ::onClickCloseAddToWalletBlock,
|
||||
isDone = isDone,
|
||||
),
|
||||
)
|
||||
}.saveIn(addToWalletBannerJobHolder)
|
||||
}
|
||||
|
||||
private fun handleBalanceHiding() {
|
||||
getBalanceHidingSettingsUseCase().onEach {
|
||||
uiState.update(DetailBalanceVisibilityTransformer(isHidden = it.isBalanceHidden))
|
||||
|
|
@ -247,6 +264,28 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}.saveIn(refreshStateJobHolder)
|
||||
}
|
||||
|
||||
private fun onClickAddToWalletBlock() {
|
||||
analytics.send(TangemPayAnalyticsEvents.AddToWalletClicked())
|
||||
router.push(TangemPayAccountDetailsInnerRoute.AddToWallet)
|
||||
}
|
||||
|
||||
private fun onClickCloseAddToWalletBlock() {
|
||||
modelScope.launch {
|
||||
try {
|
||||
cardDetailsRepository.setAddToWalletAsDone(userWalletId)
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Error", e)
|
||||
}
|
||||
uiState.update(
|
||||
transformer = DetailsAddToWalletBannerTransformer(
|
||||
onClickBanner = ::onClickAddToWalletBlock,
|
||||
onClickCloseBanner = ::onClickCloseAddToWalletBlock,
|
||||
isDone = true,
|
||||
),
|
||||
)
|
||||
}.saveIn(addToWalletBannerJobHolder)
|
||||
}
|
||||
|
||||
private fun onOpenMenu() {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardSettingsClicked())
|
||||
}
|
||||
|
|
@ -309,11 +348,18 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCardClick() {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardIconClicked())
|
||||
router.push(TangemPayAccountDetailsInnerRoute.CardDetails)
|
||||
}
|
||||
|
||||
override fun onAddCardClick() {
|
||||
uiMessageSender.send(message = TangemPayMessagesFactory.createFutureFeature())
|
||||
analytics.send(TangemPayAnalyticsEvents.AddExtraCardClicked())
|
||||
analytics.send(TangemPayAnalyticsEvents.FakeDoorPopupDisplayed())
|
||||
uiMessageSender.send(
|
||||
message = TangemPayMessagesFactory.createFutureFeature(
|
||||
onGotItClick = { analytics.send(TangemPayAnalyticsEvents.FakeDoorGotitClicked()) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBottomSheetError(type: TangemPayDetailsErrorType) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.tangempay.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -8,17 +10,21 @@ import com.tangem.core.decompose.navigation.Router
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.account.hasCardWithId
|
||||
import com.tangem.domain.models.account.requireCardWithId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM
|
||||
import com.tangem.features.tangempay.utils.firstCard
|
||||
import com.tangem.features.tangempay.utils.userWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -28,8 +34,9 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val cardDetailsRepository: TangemPayCardDetailsRepository,
|
||||
private val updateCardNameUseCase: UpdateTangemPayCardNameUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
|
||||
|
|
@ -40,7 +47,10 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
|
|||
val uiState: StateFlow<TangemPayEditDisplayNameUM>
|
||||
field = MutableStateFlow(
|
||||
TangemPayEditDisplayNameUM(
|
||||
editingValue = originalDisplayName,
|
||||
editingValue = TextFieldValue(
|
||||
text = originalDisplayName,
|
||||
selection = TextRange(originalDisplayName.length),
|
||||
),
|
||||
isLoading = false,
|
||||
onValueChanged = ::onValueChanged,
|
||||
onDoneClick = ::onDoneClick,
|
||||
|
|
@ -48,23 +58,51 @@ internal class TangemPayEditDisplayNameModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private fun onValueChanged(value: String) {
|
||||
if (value.length <= CardDisplayName.MAX_LENGTH) {
|
||||
init {
|
||||
subscribeToCardNameChanges(card.id, params.initialStatus.userWalletId)
|
||||
}
|
||||
|
||||
private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) {
|
||||
paymentAccountStatusSupplier.invoke(userWalletId)
|
||||
.onEach { state ->
|
||||
val status = state.value
|
||||
if (status is PaymentAccountStatusValue.Loaded &&
|
||||
status.source == StatusSource.ACTUAL &&
|
||||
status.hasCardWithId(cardId)
|
||||
) {
|
||||
val card = status.requireCardWithId(cardId)
|
||||
val displayName = card.displayName ?: return@onEach
|
||||
|
||||
uiState.update { uiState ->
|
||||
uiState.copy(
|
||||
editingValue = TextFieldValue(
|
||||
text = displayName.value,
|
||||
selection = TextRange(displayName.value.length),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onValueChanged(value: TextFieldValue) {
|
||||
if (value.text.length <= CardDisplayName.MAX_LENGTH) {
|
||||
uiState.update { it.copy(editingValue = value) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDoneClick() {
|
||||
val currentValue = uiState.value.editingValue
|
||||
val currentValue = uiState.value.editingValue.text
|
||||
if (currentValue.trim() == originalDisplayName.trim()) {
|
||||
router.pop()
|
||||
return
|
||||
}
|
||||
CardDisplayName(currentValue)
|
||||
.onRight { cardDisplayName ->
|
||||
uiState.update { it.copy(isLoading = true) }
|
||||
modelScope.launch {
|
||||
cardDetailsRepository.updateCardDisplayName(
|
||||
uiState.update { it.copy(isLoading = true) }
|
||||
updateCardNameUseCase(
|
||||
cardId = card.id,
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
displayName = cardDisplayName,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.features.tangempay.entity.AddToWalletBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class DetailsAddToWalletBannerTransformer(
|
||||
private val onClickBanner: () -> Unit,
|
||||
private val onClickCloseBanner: () -> Unit,
|
||||
private val isDone: Boolean,
|
||||
) : Transformer<TangemPayDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
|
||||
return prevState.copy(
|
||||
addToWalletBlockState = if (isDone) {
|
||||
null
|
||||
} else {
|
||||
AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.tangempay.model.transformers
|
||||
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class TangemPayCardDetailsUpdateNameTransformer(
|
||||
private val displayName: CardDisplayName,
|
||||
) : Transformer<TangemPayCardDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM {
|
||||
val displayNameState = prevState.displayNameState ?: return prevState
|
||||
return prevState.copy(displayNameState = displayNameState.copySealed(displayName = displayName.value))
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object CardDetails : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object AddToWallet : TangemPayAccountDetailsInnerRoute()
|
||||
}
|
||||
|
|
@ -1,24 +1,22 @@
|
|||
package com.tangem.features.tangempay.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.EaseInOut
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -46,6 +44,7 @@ import com.tangem.core.ui.components.SpacerWMax
|
|||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -58,7 +57,7 @@ import com.tangem.features.tangempay.entity.DisplayNameState
|
|||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
|
||||
private const val ICON_FADE_DURATION_MS = 300
|
||||
private const val TEXT_WIDTH_PADDING = 2
|
||||
private val CustomCardBlockColor = Color(0x1F828282)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
|
|
@ -228,80 +227,55 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
|
|||
|
||||
@Composable
|
||||
private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifier) {
|
||||
val isDisplayMode = state is DisplayNameState.Display
|
||||
|
||||
Row(
|
||||
modifier = modifier.then(
|
||||
if (state is DisplayNameState.Display) {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = state.onClick,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when (state) {
|
||||
is DisplayNameState.Display -> DisplayOnlyCardDisplayName(state = state)
|
||||
is DisplayNameState.Editing -> EditingCardDisplayName(state = state)
|
||||
}
|
||||
val iconVisibleState = remember {
|
||||
MutableTransitionState(initialState = !isDisplayMode).apply {
|
||||
targetState = isDisplayMode
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visibleState = iconVisibleState,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = ICON_FADE_DURATION_MS)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = ICON_FADE_DURATION_MS)),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Icon(
|
||||
painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_edit_new_12),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(10.dp),
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
}
|
||||
}
|
||||
when (state) {
|
||||
is DisplayNameState.Display -> DisplayOnlyCardDisplayName(modifier = modifier, state = state)
|
||||
is DisplayNameState.Editing -> EditingCardDisplayName(modifier = modifier, state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayOnlyCardDisplayName(state: DisplayNameState.Display, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = state.displayName,
|
||||
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
|
||||
maxLines = 1,
|
||||
modifier = modifier,
|
||||
)
|
||||
Row(
|
||||
modifier = modifier.conditional(
|
||||
condition = state.isEditingEnabled,
|
||||
modifier = { clickable(onClick = state.onClick) },
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = state.displayName,
|
||||
style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite),
|
||||
maxLines = 1,
|
||||
)
|
||||
if (state.isEditingEnabled) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_edit_new_12),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(10.dp),
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Modifier = Modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var textFieldValue by remember(state.editingValue) {
|
||||
mutableStateOf(
|
||||
TextFieldValue(text = state.editingValue, selection = TextRange(state.editingValue.length)),
|
||||
)
|
||||
}
|
||||
val placeholder = stringResourceSafe(R.string.tangempay_card_edit_name_placeholder)
|
||||
|
||||
val textStyle = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite)
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val measuredText = state.editingValue.text.ifEmpty { placeholder }
|
||||
val textWidthDp = with(LocalDensity.current) {
|
||||
textMeasurer.measure(textFieldValue.text, textStyle).size.width.toDp() + 2.dp
|
||||
textMeasurer.measure(measuredText, textStyle).size.width.toDp() + TEXT_WIDTH_PADDING.dp
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
value = state.editingValue,
|
||||
onValueChange = { newValue ->
|
||||
if (newValue.text.length in 0..CardDisplayName.MAX_LENGTH) {
|
||||
textFieldValue = newValue
|
||||
state.onValueChanged(newValue.text)
|
||||
if (newValue.text.length <= CardDisplayName.MAX_LENGTH) {
|
||||
state.onValueChanged(newValue)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
|
|
@ -312,6 +286,17 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
|
|||
cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { state.onSubmit() }),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (state.editingValue.text.isEmpty()) {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = textStyle.copy(color = TangemTheme.colors.text.tertiary),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
|
|
@ -492,7 +477,13 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
onCopy = { _, _ -> },
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Frozen,
|
||||
displayNameState = DisplayNameState.Display(displayName = "Tangem", onClick = {}),
|
||||
displayNameState = DisplayNameState.Editing(
|
||||
displayName = "Tangem",
|
||||
editingValue = TextFieldValue(text = "movet", selection = TextRange("movet".length)),
|
||||
onValueChanged = {},
|
||||
onSubmit = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
),
|
||||
TangemPayCardDetailsUM(
|
||||
isLoading = false,
|
||||
|
|
@ -505,7 +496,13 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
onCopy = { _, _ -> },
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
|
||||
displayNameState = DisplayNameState.Editing(
|
||||
displayName = "Tangem",
|
||||
editingValue = TextFieldValue(text = ""),
|
||||
onValueChanged = {},
|
||||
onSubmit = {},
|
||||
onDismiss = {},
|
||||
),
|
||||
),
|
||||
TangemPayCardDetailsUM(
|
||||
isLoading = false,
|
||||
|
|
@ -518,7 +515,11 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
onCopy = { _, _ -> },
|
||||
isHidden = true,
|
||||
cardFrozenState = TangemPayCardFrozenState.Pending,
|
||||
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = true,
|
||||
),
|
||||
),
|
||||
TangemPayCardDetailsUM(
|
||||
isLoading = false,
|
||||
|
|
@ -531,7 +532,11 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide
|
|||
expiry = "12/34",
|
||||
cvv = "123",
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -8,18 +8,7 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
|
|
@ -46,11 +35,7 @@ import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
|||
import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.DisplayNameState
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardPageSetting
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardPageUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState
|
||||
import com.tangem.features.tangempay.entity.*
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val CONTENT_FADE_DURATION_MS = 300
|
||||
|
|
@ -217,7 +202,11 @@ private fun preview() = TangemThemePreview {
|
|||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
cardDetailsState = TangemPayCardDetailsUM(
|
||||
|
|
@ -228,7 +217,11 @@ private fun preview() = TangemThemePreview {
|
|||
onCopy = { _, _ -> },
|
||||
onClick = {},
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}),
|
||||
displayNameState = DisplayNameState.Display(
|
||||
displayName = "Tangem Pay Card",
|
||||
onClick = {},
|
||||
isEditingEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -56,10 +56,7 @@ import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTrans
|
|||
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
|
||||
import com.tangem.features.tangempay.entity.*
|
||||
import com.tangem.features.tokendetails.ExpressTransactionsComponent
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -116,6 +113,17 @@ internal fun TangemPayDetailsScreen(
|
|||
SpacerH12()
|
||||
},
|
||||
)
|
||||
if (state.addToWalletBlockState != null) {
|
||||
item(
|
||||
key = AddToWalletBlockState::class.java,
|
||||
content = {
|
||||
TangemPayAddToWalletBlock(
|
||||
state = state.addToWalletBlockState,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (state.accountDeactivatedNotificationConfig != null) {
|
||||
item(
|
||||
key = "DEACTIVATION_MESSAGE",
|
||||
|
|
@ -422,6 +430,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
|
|||
),
|
||||
isBalanceHidden = false,
|
||||
addFundsEnabled = true,
|
||||
addToWalletBlockState = AddToWalletBlockState(onClick = {}, onClickClose = {}),
|
||||
accountDeactivatedNotificationConfig = null,
|
||||
),
|
||||
TangemPayDetailsUM(
|
||||
|
|
@ -436,6 +445,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
|
|||
),
|
||||
isBalanceHidden = false,
|
||||
addFundsEnabled = true,
|
||||
addToWalletBlockState = null,
|
||||
accountDeactivatedNotificationConfig = null,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ internal fun TangemPayEditDisplayNameScreen(
|
|||
textReference = resourceReference(R.string.common_done),
|
||||
onClick = state.onDoneClick,
|
||||
shouldShowProgress = state.isLoading,
|
||||
isEnabled = !state.isLoading && state.editingValue.isNotBlank(),
|
||||
isEnabled = !state.isLoading && state.editingValue.text.isNotBlank(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ internal object TangemPayMessagesFactory {
|
|||
}
|
||||
}
|
||||
|
||||
fun createFutureFeature(): BottomSheetMessage {
|
||||
fun createFutureFeature(onGotItClick: () -> Unit): BottomSheetMessage {
|
||||
return bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.ic_credit_card_add_24) {
|
||||
|
|
@ -125,7 +125,10 @@ internal object TangemPayMessagesFactory {
|
|||
}
|
||||
primaryButton {
|
||||
text = resourceReference(R.string.common_got_it)
|
||||
onClick { closeBs() }
|
||||
onClick {
|
||||
onGotItClick()
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ internal class TangemPayCardLimitSetupModelTest {
|
|||
) {
|
||||
val model = createModel()
|
||||
|
||||
model.uiState.value.amountFieldModel.onValueChange("100")
|
||||
model.uiState.value.amountFieldModel.onValueChange(amount)
|
||||
|
||||
assertThat(model.uiState.value.isSubmitButtonEnabled).isEqualTo(expectedEnabled)
|
||||
|
|
@ -152,7 +153,7 @@ internal class TangemPayCardLimitSetupModelTest {
|
|||
Arguments.of("100", true),
|
||||
Arguments.of("-1", false),
|
||||
Arguments.of("", false),
|
||||
Arguments.of("abc", false),
|
||||
Arguments.of("abc", true),
|
||||
Arguments.of("1001", false),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.feature.wallet.child.managetokens.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
internal sealed class PortfolioAnalyticsEvent(
|
||||
event: String,
|
||||
) : AnalyticsEvent(category = "Portfolio", event = event) {
|
||||
|
||||
class ButtonAddManage : PortfolioAnalyticsEvent(event = "Button - Add Manage")
|
||||
|
||||
class ButtonAddTokens : PortfolioAnalyticsEvent(event = "Button - Add tokens")
|
||||
|
||||
class ButtonOrganizeTokens : PortfolioAnalyticsEvent(event = "Button - Organize Tokens")
|
||||
}
|
||||
|
|
@ -3,11 +3,13 @@ package com.tangem.feature.wallet.child.managetokens.model
|
|||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
|
||||
import com.tangem.feature.wallet.child.managetokens.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
|
||||
|
|
@ -21,6 +23,7 @@ internal class AddAndManageModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -45,6 +48,7 @@ internal class AddAndManageModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onAddTokensClick() {
|
||||
analyticsEventHandler.send(PortfolioAnalyticsEvent.ButtonAddTokens())
|
||||
modelScope.launch {
|
||||
val data = portfolioFetcher.data.first()
|
||||
val isSingleAccount = data.isSingleChoice(params.userWalletId)
|
||||
|
|
@ -65,6 +69,7 @@ internal class AddAndManageModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onOrganizeTokensClick() {
|
||||
analyticsEventHandler.send(PortfolioAnalyticsEvent.ButtonOrganizeTokens())
|
||||
params.onDismiss()
|
||||
params.onOrganizeTokensClick()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,10 +260,11 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
WalletScreen2(
|
||||
state = uiState,
|
||||
tangemPayComponent = tangemPayMainBlockComponent,
|
||||
bottomSheetContent = {
|
||||
bottomSheetContent = { onExpandSheet ->
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
onExpandSheet = onExpandSheet,
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
|
|
@ -275,10 +276,11 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
state = uiState,
|
||||
promoBannersBlockComponent = promoBannersBlockComponent,
|
||||
tangemPayComponent = tangemPayMainBlockComponent,
|
||||
bottomSheetContent = {
|
||||
bottomSheetContent = { onExpandSheet ->
|
||||
BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = { headerSize = it },
|
||||
onExpandSheet = onExpandSheet,
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
|
|
@ -305,11 +307,13 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
private fun BottomSheetContent(
|
||||
bottomSheetState: State<BottomSheetState>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
onExpandSheet: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
feedEntryComponent.BottomSheetContent(
|
||||
bottomSheetState = bottomSheetState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
onExpandSheet = onExpandSheet,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
|||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase
|
||||
import com.tangem.feature.wallet.child.managetokens.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.feature.wallet.presentation.account.AccountDependencies
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
|
||||
|
|
@ -123,6 +124,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
override fun onOrganizeTokensClick() {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
if (walletFeatureToggles.isAddAndManageTokensEnabled) {
|
||||
analyticsEventHandler.send(PortfolioAnalyticsEvent.ButtonAddManage())
|
||||
router.openAddAndManageBottomSheet(userWalletId = userWalletId)
|
||||
} else {
|
||||
router.openOrganizeTokensScreen(userWalletId = userWalletId)
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ internal fun WalletScreen(
|
|||
state: WalletScreenState,
|
||||
tangemPayComponent: TangemPayMainBlockComponent,
|
||||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
) {
|
||||
|
|
@ -139,7 +139,7 @@ private fun WalletContent(
|
|||
promoBannersBlockComponent: ComposableContentComponent? = null,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
) {
|
||||
/*
|
||||
* Don't pass key to remember, because it will brake scroll animation.
|
||||
|
|
@ -295,7 +295,7 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
crossinline bottomSheetContent: @Composable () -> Unit,
|
||||
crossinline bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
crossinline content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
val isKeyboardVisible by rememberIsKeyboardVisible()
|
||||
|
|
@ -382,7 +382,7 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// expand bottom sheet when clicked on the header
|
||||
// expand bottom sheet when clicked on the drag handle
|
||||
.clickable(
|
||||
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
|
||||
indication = null,
|
||||
|
|
@ -400,7 +400,9 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
isSearchFieldFocused = it.isFocused
|
||||
},
|
||||
) {
|
||||
bottomSheetContent()
|
||||
bottomSheetContent {
|
||||
coroutineScope.launch { bottomSheetState.expand() }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ internal fun WalletScreen2(
|
|||
state: WalletScreenState,
|
||||
tangemPayComponent: TangemPayMainBlockComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
) {
|
||||
|
|
@ -162,7 +162,7 @@ private fun WalletContent2(
|
|||
modifier: Modifier = Modifier,
|
||||
bottomSheetHeaderHeightProvider: () -> Dp,
|
||||
onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
bottomSheetContent: @Composable (() -> Unit),
|
||||
bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
|
@ -354,7 +354,7 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
modifier: Modifier = Modifier,
|
||||
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
|
||||
crossinline appBarContent: @Composable () -> Unit,
|
||||
crossinline bottomSheetContent: @Composable () -> Unit,
|
||||
crossinline bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit,
|
||||
crossinline content: @Composable (PaddingValues, TangemSheetState) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
|
@ -395,7 +395,9 @@ private inline fun BaseScaffoldWithMarkets(
|
|||
isSearchFieldFocused = focusState.isFocused
|
||||
},
|
||||
) {
|
||||
bottomSheetContent()
|
||||
bottomSheetContent {
|
||||
coroutineScope.launch { bottomSheetState.expand() }
|
||||
}
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
|
|
@ -467,7 +469,7 @@ private fun BottomSheet(
|
|||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
// expand bottom sheet when clicked on the header
|
||||
// expand bottom sheet when clicked on the drag handle
|
||||
.clickable(
|
||||
enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded,
|
||||
indication = null,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
|||
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.test.MainScreenTestTags
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||
|
||||
private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton"
|
||||
|
|
@ -24,8 +25,13 @@ internal fun LazyListScope.organizeTokensButton(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) {
|
||||
val testTag = if (config.textRes == R.string.main_add_and_manage_tokens) {
|
||||
MainScreenTestTags.ADD_AND_MANAGE_BUTTON
|
||||
} else {
|
||||
MainScreenTestTags.ORGANIZE_TOKENS_BUTTON
|
||||
}
|
||||
RoundedActionButton(
|
||||
modifier = modifier.testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON),
|
||||
modifier = modifier.testTag(testTag),
|
||||
config = ActionButtonConfig(
|
||||
text = resourceReference(id = config.textRes),
|
||||
iconResId = config.iconRes,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package com.tangem.feature.wallet.child.managetokens.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher
|
||||
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class AddAndManageModelTest {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val portfolioFetcher: PortfolioFetcher = mockk(relaxed = true) {
|
||||
every { data } returns flowOf(
|
||||
PortfolioFetcher.Data(
|
||||
appCurrency = mockk(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
balances = emptyMap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
private val portfolioFetcherFactory: PortfolioFetcher.Factory = mockk(relaxed = true) {
|
||||
every { create(any(), any()) } returns portfolioFetcher
|
||||
}
|
||||
private val portfolioSelectorController: PortfolioSelectorController = mockk(relaxed = true) {
|
||||
every { selectedAccount } returns flowOf(null)
|
||||
}
|
||||
|
||||
private val onDismiss: () -> Unit = mockk(relaxed = true)
|
||||
private val onOrganizeTokensClick: () -> Unit = mockk(relaxed = true)
|
||||
private val onManageTokensClick: (AccountId) -> Unit = mockk(relaxed = true)
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF")
|
||||
|
||||
private val params = AddAndManageBottomSheetComponent.Params(
|
||||
userWalletId = userWalletId,
|
||||
onDismiss = onDismiss,
|
||||
onOrganizeTokensClick = onOrganizeTokensClick,
|
||||
onManageTokensClick = onManageTokensClick,
|
||||
)
|
||||
|
||||
private fun createModel(): AddAndManageModel = AddAndManageModel(
|
||||
paramsContainer = MutableParamsContainer(params),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
portfolioFetcherFactory = portfolioFetcherFactory,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
portfolioSelectorController = portfolioSelectorController,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN bottom sheet model WHEN onAddTokensClick THEN sends ButtonAddTokens event with correct payload`() =
|
||||
runTest {
|
||||
val model = createModel()
|
||||
val captured = slot<AnalyticsEvent>()
|
||||
|
||||
model.onAddTokensClick()
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(capture(captured)) }
|
||||
assertThat(captured.captured.category).isEqualTo("Portfolio")
|
||||
assertThat(captured.captured.event).isEqualTo("Button - Add tokens")
|
||||
assertThat(captured.captured.params).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN bottom sheet model WHEN onOrganizeTokensClick THEN sends ButtonOrganizeTokens event with correct payload`() =
|
||||
runTest {
|
||||
val model = createModel()
|
||||
val captured = slot<AnalyticsEvent>()
|
||||
|
||||
model.onOrganizeTokensClick()
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(capture(captured)) }
|
||||
assertThat(captured.captured.category).isEqualTo("Portfolio")
|
||||
assertThat(captured.captured.event).isEqualTo("Button - Organize Tokens")
|
||||
assertThat(captured.captured.params).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN bottom sheet model WHEN onOrganizeTokensClick THEN dismisses bottom sheet and forwards to params callback`() =
|
||||
runTest {
|
||||
val model = createModel()
|
||||
|
||||
model.onOrganizeTokensClick()
|
||||
|
||||
verify(exactly = 1) { onDismiss() }
|
||||
verify(exactly = 1) { onOrganizeTokensClick() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.feature.wallet.child.wallet.model.intents
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class WalletContentClickIntentsAnalyticsTest {
|
||||
|
||||
private val stateHolder: WalletStateController = mockk(relaxed = true)
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val walletFeatureToggles: WalletFeatureToggles = mockk(relaxed = true)
|
||||
private val router: InnerWalletRouter = mockk(relaxed = true)
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF")
|
||||
|
||||
private fun createImplementor(): WalletContentClickIntentsImplementor {
|
||||
every { stateHolder.getSelectedWalletId() } returns userWalletId
|
||||
|
||||
val implementor = WalletContentClickIntentsImplementor(
|
||||
stateHolder = stateHolder,
|
||||
currencyActionsClickIntents = mockk(relaxed = true),
|
||||
onrampStatusFactory = mockk(relaxed = true),
|
||||
getUserWalletUseCase = mockk(relaxed = true),
|
||||
singleAccountStatusListSupplier = mockk(relaxed = true),
|
||||
getCryptoCurrencyActionsUseCase = mockk(relaxed = true),
|
||||
getExplorerTransactionUrlUseCase = mockk(relaxed = true),
|
||||
shouldShowMarketsTooltipUseCase = mockk(relaxed = true),
|
||||
dispatchers = mockk(relaxed = true),
|
||||
walletEventSender = mockk(relaxed = true),
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
accountDependencies = mockk(relaxed = true),
|
||||
yieldSupplySetShouldShowMainPromoUseCase = mockk(relaxed = true),
|
||||
tokenListAnalyticsSender = mockk(relaxed = true),
|
||||
uiMessageSender = mockk(relaxed = true),
|
||||
walletFeatureToggles = walletFeatureToggles,
|
||||
)
|
||||
implementor.initialize(router = router, coroutineScope = TestScope())
|
||||
return implementor
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN add and manage toggle enabled WHEN onOrganizeTokensClick THEN sends ButtonAddManage event and opens bottom sheet`() =
|
||||
runTest {
|
||||
every { walletFeatureToggles.isAddAndManageTokensEnabled } returns true
|
||||
val implementor = createImplementor()
|
||||
val captured = slot<AnalyticsEvent>()
|
||||
|
||||
implementor.onOrganizeTokensClick()
|
||||
|
||||
verify(exactly = 1) { analyticsEventHandler.send(capture(captured)) }
|
||||
assertThat(captured.captured.category).isEqualTo("Portfolio")
|
||||
assertThat(captured.captured.event).isEqualTo("Button - Add Manage")
|
||||
assertThat(captured.captured.params).isEmpty()
|
||||
verify(exactly = 1) { router.openAddAndManageBottomSheet(userWalletId = userWalletId) }
|
||||
verify(exactly = 0) { router.openOrganizeTokensScreen(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN add and manage toggle disabled WHEN onOrganizeTokensClick THEN does not send analytics and opens organize screen`() =
|
||||
runTest {
|
||||
every { walletFeatureToggles.isAddAndManageTokensEnabled } returns false
|
||||
val implementor = createImplementor()
|
||||
|
||||
implementor.onOrganizeTokensClick()
|
||||
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any<AnalyticsEvent>()) }
|
||||
verify(exactly = 1) { router.openOrganizeTokensScreen(userWalletId = userWalletId) }
|
||||
verify(exactly = 0) { router.openAddAndManageBottomSheet(any()) }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue