Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-28 15:57:21 +03:00
commit 31be70ecf3
529 changed files with 15730 additions and 4170 deletions

View file

@ -113,12 +113,14 @@ dependencies {
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk) implementation(projects.libs.blockchainSdk)
implementation(projects.domain.account) implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.core) implementation(projects.domain.core)
api(projects.domain.common) api(projects.domain.common)
implementation(projects.domain.card) implementation(projects.domain.card)
implementation(projects.domain.demo) implementation(projects.domain.demo)
implementation(projects.domain.demo.models) implementation(projects.domain.demo.models)
implementation(projects.domain.express)
implementation(projects.domain.wallets) implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.settings) implementation(projects.domain.settings)

View file

@ -4,8 +4,21 @@ object TestConstants {
const val TOTAL_BALANCE = "$3,299.18" const val TOTAL_BALANCE = "$3,299.18"
const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0" const val RECIPIENT_ADDRESS = "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0"
const val ETHEREUM_ADDRESS = "0x9FFd974772bDA94D288240C1B22F367Ce75CcD7f"
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3" const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val CARDANO_ADDRESS = "addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p" const val CARDANO_ADDRESS =
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
const val POLKADOT_RECIPIENT_ADDRESS = "143TfgFYAFfM86LRzt4UcFNU3KosxCndBCVz2U5HCxpLidKZ"
const val XRP_RECIPIENT_ADDRESS = "rNeY28BPda6jp5N5oESZzd2ZN7eMZy8jNf"
const val ENS_NAME = "louded.eth"
const val ENS_ETHEREUM_RECIPIENT_ADDRESS = "0x0211ff638298adcbdc99c177dc2e95ec69948865"
const val ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x0211ff63829..."
const val KUSAMA_RECIPIENT_ADDRESS = "CqNrR92Hh76vW69vDBL5iATrZoYkk9nj67iVSUbb2YHtktn"
const val AZERO_RECIPIENT_ADDRESS = "5EA4p6DZdbt2vLZySML2dG3ZsnNrenEWZHnVCScQh4iq2KZo"
const val TEZOS_RECIPIENT_ADDRESS = "tz1eBdC2JkU2bxgZssweLo6D3wCkWN12ioHW"
const val WAIT_UNTIL_TIMEOUT = 20_000L const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
@ -14,4 +27,7 @@ object TestConstants {
const val ALLURE_LABEL_NAME = "Owner" const val ALLURE_LABEL_NAME = "Owner"
const val ALLURE_LABEL_VALUE = "Kaspresso" const val ALLURE_LABEL_VALUE = "Kaspresso"
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val QUOTES_API_SCENARIO = "quotes_api"
} }

View file

@ -20,12 +20,12 @@ fun BaseTestCase.swipeVertical(
) )
} }
fun BaseTestCase.pullToRefresh() { fun BaseTestCase.pullToRefresh(steps: Int = 1000) {
swipeVertical( swipeVertical(
direction = SwipeDirection.DOWN, direction = SwipeDirection.DOWN,
startHeightRatio = 0.2f, startHeightRatio = 0.2f,
endHeightRatio = 0.8f, endHeightRatio = 0.8f,
steps = 1000 steps = steps
) )
} }

View file

@ -115,4 +115,28 @@ fun BaseTestCase.checkMultiCurrencyMainScreen(
step("Assert 'Organize tokens' button is displayed") { step("Assert 'Organize tokens' button is displayed") {
onMainScreen { organizeTokensButton().assertIsDisplayed() } onMainScreen { organizeTokensButton().assertIsDisplayed() }
} }
}
fun BaseTestCase.assertActionButtonsForMultiCurrencyWallet(isEnabled: Boolean = true) {
if (isEnabled) {
step("Assert 'Buy' button is enabled") {
onMainScreen { buyButton.assertIsEnabled() }
}
step("Assert 'Swap' button is enabled") {
onMainScreen { swapButton.assertIsEnabled() }
}
step("Assert 'Sell' button is enabled") {
onMainScreen { sellButton.assertIsEnabled() }
}
} else {
step("Assert 'Buy' button is not enabled") {
onMainScreen { buyButton.assertIsNotEnabled() }
}
step("Assert 'Swap' button is not enabled") {
onMainScreen { swapButton.assertIsNotEnabled() }
}
step("Assert 'Sell' button is not enabled") {
onMainScreen { sellButton.assertIsNotEnabled() }
}
}
} }

View file

@ -8,6 +8,7 @@ import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
import com.tangem.screens.ScanWarningDialogPageObject import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onActionIsUnavailableDialog
import com.tangem.screens.onFailedTransactionDialog import com.tangem.screens.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step import io.qameta.allure.kotlin.Allure.step
@ -63,4 +64,16 @@ fun checkAlreadyUsedWalletDialog() {
step("Assert 'Request support' button is displayed") { step("Assert 'Request support' button is displayed") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() } AlreadyUsedWalletDialogPageObject { requestSupportButton.isDisplayed() }
} }
}
fun BaseTestCase.checkActionIsUnavailableDialog() {
step("Assert 'Action is unavailable' dialog title is displayed") {
onActionIsUnavailableDialog { title.assertIsDisplayed() }
}
step("Assert 'Action is unavailable' dialog text is displayed") {
onActionIsUnavailableDialog { text.assertIsDisplayed() }
}
step("Assert 'Action is unavailable' dialog 'Ok' button is displayed") {
onActionIsUnavailableDialog { okButton.assertIsDisplayed() }
}
} }

View file

@ -0,0 +1,41 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.onReceiveAssetsBottomSheet
import com.tangem.screens.onTokenReceiveQrCodeBottomSheet
import com.tangem.screens.onTokenReceiveWarningBottomSheet
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.goToQrCodeBottomSheet() {
step("Assert 'Token receive warning' bottom sheet is displayed") {
onTokenReceiveWarningBottomSheet { bottomSheet.assertIsDisplayed() }
}
step("Click on 'Got it' button") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() }
}
step("Click on 'Show QR code' button") {
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
}
}
fun BaseTestCase.checkQrCodeBottomSheetScenario() {
step("Assert bottom sheet with QR code title is displayed") {
onTokenReceiveQrCodeBottomSheet { title.assertIsDisplayed() }
}
step("Assert QR code is displayed") {
onTokenReceiveQrCodeBottomSheet { qrCode.assertIsDisplayed() }
}
step("Assert address title is displayed") {
onTokenReceiveQrCodeBottomSheet { addressTitle.assertIsDisplayed() }
}
step("Assert address is displayed") {
onTokenReceiveQrCodeBottomSheet { address.assertIsDisplayed() }
}
step("Assert 'Copy' button is displayed") {
onTokenReceiveQrCodeBottomSheet { copyButton.assertIsDisplayed() }
}
step("Assert 'Share' button is displayed") {
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,84 @@
package com.tangem.scenarios
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onTokenDetailsScreen
import io.github.kakaocup.compose.node.element.KNode
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openSendScreen(tokenName: String) {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokenName)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: '$tokenName'") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokenName)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
}
fun BaseTestCase.checkSendWarning(
titleResId: Int,
messageResId: Int,
amount: String,
isDisplayed: Boolean = true,
sendButtonIsDisabled: Boolean = isDisplayed,
) {
val assertDisplay = if (isDisplayed) "displayed" else "not displayed"
step("Assert 'Send confirm screen' is displayed") {
onSendConfirmScreen {
title.assertIsDisplayed()
}
}
step("Assert warning title is $assertDisplay") {
onSendConfirmScreen {
warningTitle(titleResId).assertVisibility(isDisplayed)
}
}
step("Assert warning icon is $assertDisplay") {
onSendConfirmScreen {
sendWarningIcon(messageResId, amount).assertVisibility(isDisplayed)
}
}
step("Assert warning message is $assertDisplay") {
onSendConfirmScreen {
sendWarningMessage(messageResId, amount).assertVisibility(isDisplayed)
}
}
if (sendButtonIsDisabled)
step("Assert 'Send' button is disabled") {
onSendConfirmScreen {
sendButton.assertIsNotEnabled()
}
}
else
step("Assert 'Send' button is enabled") {
onSendConfirmScreen {
sendButton.assertIsEnabled()
}
}
}
private fun KNode.assertVisibility(shouldBeDisplayed: Boolean) {
if (shouldBeDisplayed) {
assertIsDisplayed()
} else {
assertIsNotDisplayed()
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
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.common.ui.R as CommonUIR
class ActionIsUnavailableDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ActionIsUnavailableDialogPageObject>(semanticsProvider = semanticsProvider) {
val dialogContainer: KNode = child {
hasTestTag(BaseDialogTestTags.CONTAINER)
}
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
hasText(getResourceString(CommonUIR.string.action_buttons_something_wrong_alert_title))
useUnmergedTree = true
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.action_buttons_something_wrong_alert_message))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onActionIsUnavailableDialog(function: ActionIsUnavailableDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -17,6 +17,14 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseDialogTestTags.CONTAINER) hasTestTag(BaseDialogTestTags.CONTAINER)
} }
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
}
val cancelButton: KNode = child { val cancelButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_cancel)) hasText(getResourceString(R.string.common_cancel))

View file

@ -0,0 +1,30 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
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.common.ui.R as CommonUIR
class OperationIsUnavailableDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<OperationIsUnavailableDialogPageObject>(semanticsProvider = semanticsProvider) {
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.token_button_unavailability_generic_description))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onOperationIsUnavailableDialog(function: OperationIsUnavailableDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,24 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.*
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SellPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SellPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_sell))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSellScreen(function: SellPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -9,18 +9,88 @@ import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.core.ui.R as CoreUiR
import androidx.compose.ui.test.hasText as withText
import androidx.compose.ui.test.hasTestTag as withTestTag
class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SendAddressPageObject>(semanticsProvider = semanticsProvider) {
val addressTextFieldTitle: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE)
useUnmergedTree = true
}
val addressTextField: KNode = child { val addressTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD) hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
useUnmergedTree = true useUnmergedTree = true
} }
val addressPasteButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.ADDRESS_PASTE_BUTTON)
useUnmergedTree = true
}
val clearTextFieldButton: KNode = child {
hasContentDescription(getResourceString(CoreUiR.string.common_close))
useUnmergedTree = true
}
val resolvedAddress: KNode = child {
hasTestTag(SendAddressScreenTestTags.RESOLVED_ADDRESS)
}
val recentAddressesTitle: KNode = child {
hasText(getResourceString(CoreUiR.string.send_recent_transactions))
useUnmergedTree = true
}
fun destinationTagBlockTitle(isMemoCorrectOrEmpty: Boolean = true): KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD_TITLE)
useUnmergedTree = true
if(isMemoCorrectOrEmpty) {
hasText(getResourceString(CoreUiR.string.send_destination_tag_field))
} else {
hasText(getResourceString(CoreUiR.string.send_memo_destination_tag_error))
}
}
val destinationTagTextField: KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD)
useUnmergedTree = true
}
val destinationTagBlockText: KNode = child {
hasText(
getResourceString(CoreUiR.string.send_recipient_memo_footer_v2) + "\n" +
getResourceString(CoreUiR.string.send_recipient_memo_footer_v2_highlighted)
)
useUnmergedTree = true
}
val destinationTagBlockCaution: KNode = child {
hasText(getResourceString(CoreUiR.string.send_recipient_memo_footer_v2_highlighted))
}
val destinationTagPasteButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_PASTE_BUTTON)
useUnmergedTree = true
}
val clearDestinationTagTextFieldButton: KNode = child {
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_CLEAR_TEXT_FIELD_BUTTON)
useUnmergedTree = true
}
val nextButton: KNode = child { val nextButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_next)) hasAnyDescendant(withText(getResourceString(R.string.common_next)))
useUnmergedTree = true
}
fun recentAddressWithText(recipientAddress: String): KNode = child {
hasParent(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TITLE))
hasText(recipientAddress)
useUnmergedTree = true useUnmergedTree = true
} }

View file

@ -2,17 +2,13 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R import com.tangem.core.ui.test.*
import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.wallet.R
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.test.SendConfirmScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText import androidx.compose.ui.test.hasText as withText
import com.tangem.common.ui.R as CommonUiR
class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) { ComposeScreen<SendConfirmPageObject>(semanticsProvider = semanticsProvider) {
@ -23,15 +19,31 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
} }
val sendButton: KNode = child { val sendButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT) hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_send)) hasAnyDescendant(withText(getResourceString(R.string.common_send)))
useUnmergedTree = true useUnmergedTree = true
} }
val primaryAmount: KNode = child {
hasTestTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT)
useUnmergedTree = true
}
val minimumSendAmountErrorTitle: KNode = child { fun leaveDepositButton(amount: String): KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.send_notification_leave_button, amount)))
useUnmergedTree = true
}
fun reduceAmountButton(amount: String): KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyDescendant(withText(getResourceString(R.string.send_notification_reduce_by, amount)))
useUnmergedTree = true
}
fun warningTitle(titleResId: Int): KNode = child {
hasTestTag(NotificationTestTags.TITLE) hasTestTag(NotificationTestTags.TITLE)
hasText(getResourceString(CommonUiR.string.send_notification_invalid_amount_title)) hasText(getResourceString(titleResId))
useUnmergedTree = true useUnmergedTree = true
} }
@ -40,27 +52,29 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true useUnmergedTree = true
} }
fun minimumSendAmountErrorIcon(amount: String): KNode = child { fun sendWarningIcon(messageResId: Int, amount: String): KNode = child {
hasTestTag(NotificationTestTags.ICON)
hasAnySibling( hasAnySibling(
withText( withText(
getResourceString( getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text, messageResId,
amount, amount,
amount, amount,
) )
) )
) )
hasTestTag(NotificationTestTags.ICON)
useUnmergedTree = true useUnmergedTree = true
} }
fun minimumSendAmountErrorMessage( fun sendWarningMessage(
messageResId: Int,
amount: String, amount: String,
): KNode = child { ): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE) hasTestTag(NotificationTestTags.MESSAGE)
hasText( hasText(
getResourceString( getResourceString(
CommonUiR.string.send_notification_invalid_minimum_amount_text, messageResId,
amount, amount,
amount, amount,
) )
@ -68,6 +82,16 @@ class SendConfirmPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
useUnmergedTree = true useUnmergedTree = true
} }
fun recipientAddress(recipientAddress: String): KNode = child {
hasTestTag(SendConfirmScreenTestTags.RECIPIENT_ADDRESS)
hasText(recipientAddress)
useUnmergedTree = true
}
val blockchainAddress: KNode = child {
hasTestTag(SendConfirmScreenTestTags.BLOCKCHAIN_ADDRESS)
useUnmergedTree = true
}
} }
internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) = internal fun BaseTestCase.onSendConfirmScreen(function: SendConfirmPageObject.() -> Unit) =

View file

@ -64,6 +64,12 @@ class SendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
useUnmergedTree = true useUnmergedTree = true
} }
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(SendR.string.common_continue))
useUnmergedTree = true
}
} }
internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) = internal fun BaseTestCase.onSendScreen(function: SendPageObject.() -> Unit) =

View file

@ -0,0 +1,35 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
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.common.ui.R as CommonUIR
class SwapIsNotSupportedDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapIsNotSupportedDialogPageObject>(semanticsProvider = semanticsProvider) {
fun text(currencyName: String): KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(
getResourceString(
CommonUIR.string.token_button_unavailability_reason_not_exchangeable,
currencyName
)
)
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onSwapIsNotSupportedDialog(function: SwapIsNotSupportedDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -97,25 +97,31 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide
) )
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val swapButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun receiveButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_receive))
}
@OptIn(ExperimentalTestApi::class)
fun swapButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_swap)) hasText(getResourceString(R.string.common_swap))
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val sellButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun sellButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_sell)) hasText(getResourceString(R.string.common_sell))
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val buyButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun buyButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_buy)) hasText(getResourceString(R.string.common_buy))
} }
@OptIn(ExperimentalTestApi::class) @OptIn(ExperimentalTestApi::class)
val sendButton: LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> { fun sendButton(): LazyListItemNode = horizontalActionChips.childWith<LazyListItemNode> {
hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON) hasTestTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON)
hasText(getResourceString(R.string.common_send)) hasText(getResourceString(R.string.common_send))
} }

View file

@ -4,11 +4,16 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.pullToRefresh import com.tangem.common.extensions.pullToRefresh
import com.tangem.common.ui.R
import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.* import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
@ -29,6 +34,9 @@ class BlockchainTest : BaseTestCase() {
val scenarioName = "user_tokens_api" val scenarioName = "user_tokens_api"
val scenarioState = "Cardano" val scenarioState = "Cardano"
val invalidAmountTitleResId = R.string.send_notification_invalid_amount_title
val invalidAmountMessageResId = R.string.send_notification_invalid_minimum_amount_text
setupHooks( setupHooks(
additionalAfterSection = { additionalAfterSection = {
resetWireMockScenarioState(scenarioName) resetWireMockScenarioState(scenarioName)
@ -47,7 +55,7 @@ class BlockchainTest : BaseTestCase() {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
} }
step("Click on 'Send' button") { step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() } onTokenDetailsScreen { sendButton().performClick() }
} }
step("Type '$errorSendAmount' in input text field") { step("Type '$errorSendAmount' in input text field") {
onSendScreen { onSendScreen {
@ -64,14 +72,12 @@ class BlockchainTest : BaseTestCase() {
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() } onSendAddressScreen { nextButton.clickWithAssertion() }
} }
step("Assert 'Invalid amount' error title is displayed") { step("Assert 'Invalid amount warning' is displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsDisplayed() } checkSendWarning(
} titleResId = invalidAmountTitleResId,
step("Assert 'Invalid amount' error icon is displayed") { messageResId = invalidAmountMessageResId,
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsDisplayed() } amount = minAmount
} )
step("Assert 'Invalid amount' error message is displayed") {
onSendConfirmScreen { minimumSendAmountErrorMessage(minAmount).assertIsDisplayed() }
} }
step("Press system 'Back' button") { step("Press system 'Back' button") {
device.uiDevice.pressBack() device.uiDevice.pressBack()
@ -97,14 +103,13 @@ class BlockchainTest : BaseTestCase() {
step("Click on 'Next' button") { step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() } onSendAddressScreen { nextButton.clickWithAssertion() }
} }
step("Assert 'Invalid amount' error title is not displayed") { step("Assert 'Invalid amount warning' is not displayed") {
onSendConfirmScreen { minimumSendAmountErrorTitle.assertIsNotDisplayed() } checkSendWarning(
} titleResId = invalidAmountTitleResId,
step("Assert 'Invalid amount' error icon is not displayed") { messageResId = invalidAmountMessageResId,
onSendConfirmScreen { minimumSendAmountErrorIcon(minAmount).assertIsNotDisplayed() } amount = minAmount,
} isDisplayed = false
step("Assert 'Invalid amount' error message is not displayed") { )
onSendConfirmScreen { minimumSendAmountErrorMessage(minAmount).assertIsNotDisplayed() }
} }
} }
} }
@ -136,10 +141,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
} }
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoErrorState'") { step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountInfoScenarioName, state = rippleAccountInfoErrorState) setWireMockScenarioState(
scenarioName = rippleAccountInfoScenarioName,
state = rippleAccountInfoErrorState
)
} }
step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesErrorState'") { step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesErrorState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesErrorState) setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesErrorState
)
} }
step("Open 'Main Screen'") { step("Open 'Main Screen'") {
openMainScreen() openMainScreen()
@ -169,10 +180,16 @@ class BlockchainTest : BaseTestCase() {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState) setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
} }
step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoStartedState'") { step("Set WireMock scenario: '$rippleAccountInfoScenarioName' to state: '$rippleAccountInfoStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountInfoScenarioName, state = rippleAccountInfoStartedState) setWireMockScenarioState(
scenarioName = rippleAccountInfoScenarioName,
state = rippleAccountInfoStartedState
)
} }
step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesStartedState'") { step("Set WireMock scenario: '$rippleAccountLinesScenarioName' to state: '$rippleAccountLinesStartedState'") {
setWireMockScenarioState(scenarioName = rippleAccountLinesScenarioName, state = rippleAccountLinesStartedState) setWireMockScenarioState(
scenarioName = rippleAccountLinesScenarioName,
state = rippleAccountLinesStartedState
)
} }
step("Pull to refresh") { step("Pull to refresh") {
pullToRefresh() pullToRefresh()

View file

@ -15,7 +15,6 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.Allure
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test import org.junit.Test
@ -73,7 +72,7 @@ class FeedbackTest : BaseTestCase() {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
} }
step("Click 'Send' button") { step("Click 'Send' button") {
onTokenDetailsScreen { sendButton.performClick() } onTokenDetailsScreen { sendButton().performClick() }
} }
step("Type '$sendAmount' in input text field") { step("Type '$sendAmount' in input text field") {
onSendScreen { onSendScreen {
@ -130,7 +129,7 @@ class FeedbackTest : BaseTestCase() {
MockProvider.resetEmulateError() MockProvider.resetEmulateError()
} }
).run { ).run {
Allure.step("Click on 'Accept' button") { step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() } onDisclaimerScreen { acceptButton.clickWithAssertion() }
} }
step("Set scanning error") { step("Set scanning error") {

View file

@ -47,7 +47,7 @@ class SwapTokenTest : BaseTestCase() {
onTokenDetailsScreen { title.assertIsDisplayed() } onTokenDetailsScreen { title.assertIsDisplayed() }
} }
step("Click on 'Swap' button") { step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() } onTokenDetailsScreen { swapButton().performClick() }
} }
step("Close 'Stories' screen") { step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() } onSwapStoriesScreen { closeButton.clickWithAssertion() }
@ -131,7 +131,7 @@ class SwapTokenTest : BaseTestCase() {
disableMobileData() disableMobileData()
} }
step("Click on 'Swap' button") { step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() } onTokenDetailsScreen { swapButton().performClick() }
} }
step("Close 'Stories' screen") { step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() } onSwapStoriesScreen { closeButton.clickWithAssertion() }
@ -175,7 +175,7 @@ class SwapTokenTest : BaseTestCase() {
onTokenDetailsScreen { title.assertIsDisplayed() } onTokenDetailsScreen { title.assertIsDisplayed() }
} }
step("Click on 'Swap' button") { step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton.performClick() } onTokenDetailsScreen { swapButton().performClick() }
} }
step("Close 'Stories' screen") { step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() } onSwapStoriesScreen { closeButton.clickWithAssertion() }

View file

@ -2,14 +2,19 @@ package com.tangem.tests.actionButtons
import androidx.compose.ui.test.longClick import androidx.compose.ui.test.longClick
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.assertClipboardTextEquals import com.tangem.common.extensions.*
import com.tangem.common.utils.clearClipboard import com.tangem.common.utils.*
import com.tangem.scenarios.openMainScreen import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.scenarios.synchronizeAddresses import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.*
import com.tangem.screens.* import com.tangem.screens.*
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.content.TwinsMockContent
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName import io.qameta.allure.kotlin.junit4.DisplayName
@ -281,41 +286,18 @@ class MainScreenActionButtonsTest : BaseTestCase() {
step("Click on 'Receive' button") { step("Click on 'Receive' button") {
onTokenActionsBottomSheet { receiveButton.performClick() } onTokenActionsBottomSheet { receiveButton.performClick() }
} }
step("Assert 'Token receive warning' bottom sheet is displayed") { step("Go to QR code bottom sheet") {
waitForIdle()
flakySafely(WAIT_UNTIL_TIMEOUT) { flakySafely(WAIT_UNTIL_TIMEOUT) {
onTokenReceiveWarningBottomSheet { goToQrCodeBottomSheet()
bottomSheet.assertIsDisplayed()
}
} }
} }
step("Click on 'Got it' button") { step("Check QR code bottom sheet") {
onTokenReceiveWarningBottomSheet { gotItButton.performClick() } checkQrCodeBottomSheetScenario()
}
step("Click on 'Show QR code' button") {
onReceiveAssetsBottomSheet { showQrCodeButton.clickWithAssertion() }
}
step("Assert bottom sheet with QR code title is displayed") {
onTokenReceiveQrCodeBottomSheet { title.assertIsDisplayed() }
}
step("Assert QR code is displayed") {
onTokenReceiveQrCodeBottomSheet { qrCode.assertIsDisplayed() }
}
step("Assert address title is displayed") {
onTokenReceiveQrCodeBottomSheet { addressTitle.assertIsDisplayed() }
}
step("Assert address is displayed") {
onTokenReceiveQrCodeBottomSheet { address.assertIsDisplayed() }
}
step("Assert 'Copy' button is displayed") {
onTokenReceiveQrCodeBottomSheet { copyButton.assertIsDisplayed() }
}
step("Assert 'Share' button is displayed") {
onTokenReceiveQrCodeBottomSheet { shareButton.assertIsDisplayed() }
} }
} }
} }
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("85") @AllureId("85")
@DisplayName("Action buttons (long tap): check 'Sell' button") @DisplayName("Action buttons (long tap): check 'Sell' button")
@Test @Test
@ -342,10 +324,10 @@ class MainScreenActionButtonsTest : BaseTestCase() {
} }
} }
} }
step("Assert 'Receive' button is displayed") { step("Assert 'Sell' button is displayed") {
onTokenActionsBottomSheet { sellButton.assertIsDisplayed() } onTokenActionsBottomSheet { sellButton.assertIsDisplayed() }
} }
step("Click on 'Receive' button") { step("Click on 'Sell' button") {
onTokenActionsBottomSheet { sellButton.performClick() } onTokenActionsBottomSheet { sellButton.performClick() }
} }
step("Assert Chrome Browser is opened") { step("Assert Chrome Browser is opened") {
@ -391,4 +373,258 @@ class MainScreenActionButtonsTest : BaseTestCase() {
} }
} }
} }
@AllureId("895")
@DisplayName("Action buttons: check blockchain information by click on 'Buy' button")
@Test
fun checkClickOnBuyButtonOnMainTest() {
val cardType: MockContent = TwinsMockContent
val cardName = "Twin"
val tokenTitle = "Bitcoin"
val tokenSymbol = "BTC"
setupHooks().run {
step("Open 'Main Screen' on '$cardName' card") {
openMainScreen(mockContent = cardType, isTwinsCard = true)
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Click on 'Confirm' button in 'Dialog'") {
waitForIdle()
onDialog { confirmButton.clickWithAssertion() }
}
step("Assert top app bar title contains '$tokenTitle'") {
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
}
step("Assert fiat currency text field is displayed") {
onBuyTokenDetailsScreen { fiatAmountTextField.assertIsDisplayed() }
}
step("Assert fiat currency icon is displayed") {
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
}
step("Assert token amount field is displayed") {
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenSymbol, substring = true) }
}
step("Assert 'Continue' button") {
onBuyTokenDetailsScreen { continueButton.assertIsDisplayed() }
}
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.MoonPay, ApiEnvironment.PROD))
@AllureId("4395")
@DisplayName("Action buttons (main screen): click on buttons with success response")
@Test
fun clickOnActionButtonsWithSuccessResponseTest() {
val tokenTitle = "Ethereum"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Assert 'Buy' screen title is displayed") {
onBuyTokenScreen { topAppBarTitle.assertIsDisplayed() }
}
step("Assert token with title: '$tokenTitle' is displayed") {
onBuyTokenScreen { tokenWithTitleAndFiatAmount(tokenTitle).assertIsDisplayed() }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Click on close button on stories screen") {
onSwapStoriesScreen { closeButton.performClick() }
}
step("Assert 'Swap' token screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Assert 'Sell' token screen title is displayed") {
onSellScreen { title.assertIsDisplayed() }
}
}
}
@AllureId("4396")
@DisplayName("Action buttons (main screen): click on buttons without data")
@Test
fun clickOnActionButtonsWithoutDataTest() {
setupHooks(
additionalAfterSection = {
enableWiFi()
enableMobileData()
}
).run {
step("Turn off internet") {
disableWiFi()
disableMobileData()
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
}
}
@AllureId("4398")
@DisplayName("Action buttons (main screen): click on buttons with error response")
@Test
fun clickOnActionButtonsWithErrorResponseTest() {
val scenarioName = "express_api_assets"
val scenarioState = "Error"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert 'Buy' button is displayed") {
onMainScreen { buyButton.assertIsDisplayed() }
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Swap' button is displayed") {
onMainScreen { swapButton.assertIsDisplayed() }
}
step("Click on 'Swap' button") {
onMainScreen { swapButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
step("Assert 'Sell' button is displayed") {
onMainScreen { sellButton.assertIsDisplayed() }
}
step("Click on 'Sell' button") {
onMainScreen { sellButton.performClick() }
}
step("Check 'Action is unavailable' dialog") {
checkActionIsUnavailableDialog()
}
step("Click on 'Ok' button") {
onDialog { okButton.performClick() }
}
}
}
@AllureId("3642")
@DisplayName("Action buttons (main screen): check buttons state")
@Test
fun checkButtonsStateTest() {
val scenarioName = "user_tokens_api"
val scenarioState = "EmptyTokensList"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
step("Reset Wiremock scenario: '$scenarioName'") {
resetWireMockScenarioState(scenarioName)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = true)
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Perform pull to refresh") {
pullToRefresh(steps = 10)
waitForIdle()
}
step("Assert action buttons is not enabled") {
assertActionButtonsForMultiCurrencyWallet(isEnabled = false)
}
}
}
} }

View file

@ -0,0 +1,255 @@
package com.tangem.tests.actionButtons
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.checkQrCodeBottomSheetScenario
import com.tangem.scenarios.goToQrCodeBottomSheet
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSwapStoriesScreen
import com.tangem.screens.onSwapTokenScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
@AllureId("594")
@DisplayName("Action buttons (token details screen): validate UI")
@Test
fun actionButtonsValidateUiTest() {
val tokenTitle = "Bitcoin"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is displayed") {
onTokenDetailsScreen { receiveButton().assertIsDisplayed() }
}
step("Assert 'Buy' button is displayed") {
onTokenDetailsScreen { buyButton().assertIsDisplayed() }
}
step("Assert 'Send' button is displayed") {
onTokenDetailsScreen { sendButton().assertIsDisplayed() }
}
step("Assert 'Swap' button is displayed") {
onTokenDetailsScreen { swapButton().assertIsDisplayed() }
}
step("Assert 'Sell' button is displayed") {
onTokenDetailsScreen { sellButton().assertIsDisplayed() }
}
}
}
@AllureId("593")
@DisplayName("Action buttons (token details screen): check buttons state")
@Test
fun checkActionButtonsStateTest() {
val tokenTitle = "Bitcoin"
val actionButtonIsNotDimmed = "Action button is not dimmed"
val actionButtonIsDimmed = "Action button is dimmed"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is not dimmed") {
onTokenDetailsScreen { receiveButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
}
step("Assert 'Buy' button is not dimmed") {
onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
}
step("Assert 'Send' button is dimmed") {
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Assert 'Sell' button is dimmed") {
onTokenDetailsScreen { sellButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
}
}
@AllureId("4459")
@DisplayName("Action buttons (token details screen): check 'Swap' button (success)")
@Test
fun checkSwapButtonSuccessTest() {
val tokenTitle = "Ethereum"
val tokenSymbol = "ETH"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton().performClick() }
}
step("Close 'Stories' screen") {
onSwapStoriesScreen { closeButton.clickWithAssertion() }
}
step("Assert 'Swap' screen title is displayed") {
onSwapTokenScreen { title.assertIsDisplayed() }
}
step("Assert token symbol: '$tokenSymbol' is displayed") {
onSwapTokenScreen { tokenSymbol(tokenSymbol).assertIsDisplayed() }
}
}
}
@AllureId("4460")
@DisplayName("Action buttons (token details screen): check 'Swap' button (provider error)")
@Test
fun checkSwapButtonProviderErrorTest() {
val tokenTitle = "POL (ex-MATIC)"
val actionButtonIsDimmed = "Action button is dimmed"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton().performClick() }
}
step("Assert swapping $tokenTitle is not supported dialog text is displayed") {
onSwapIsNotSupportedDialog { text(tokenTitle).assertIsDisplayed() }
}
step("Assert 'Ok' button is displayed") {
onSwapIsNotSupportedDialog { okButton.assertIsDisplayed() }
}
step("Click on 'Ok' button") {
onSwapIsNotSupportedDialog { okButton.performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
}
}
@AllureId("4461")
@DisplayName("Action buttons (token details screen): check 'Swap' button (Express error)")
@Test
fun checkSwapButtonExpressErrorTest() {
val tokenTitle = "Polygon"
val actionButtonIsDimmed = "Action button is dimmed"
val scenarioName = "express_api_assets"
val scenarioState = "Error"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName, scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
step("Click on 'Swap' button") {
onTokenDetailsScreen { swapButton().performClick() }
}
step("Assert operation is unavailable dialog text is displayed") {
onOperationIsUnavailableDialog { text.assertIsDisplayed() }
}
step("Assert 'Ok' button is displayed") {
onOperationIsUnavailableDialog { okButton.assertIsDisplayed() }
}
step("Click on 'Ok' button") {
onOperationIsUnavailableDialog { okButton.performClick() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
}
}
}
@AllureId("3590")
@DisplayName("Action buttons (token details screen): validate UI")
@Test
fun checkReceiveButtonTest() {
val tokenTitle = "Bitcoin"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is displayed") {
onTokenDetailsScreen { receiveButton().performClick() }
}
step("Go to QR code bottom sheet") {
flakySafely(WAIT_UNTIL_TIMEOUT) {
goToQrCodeBottomSheet()
}
}
step("Check QR code bottom sheet") {
checkQrCodeBottomSheetScenario()
}
}
}
}

View file

@ -0,0 +1,341 @@
package com.tangem.tests.send.addressScreen
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
import com.tangem.common.constants.TestConstants.ENS_NAME
import com.tangem.common.constants.TestConstants.ETHEREUM_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
import com.tangem.common.constants.TestConstants.XRP_RECIPIENT_ADDRESS
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.clearClipboard
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setClipboardText
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.kakao.common.utilities.getResourceString
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SendAddressScreenTest : BaseTestCase() {
@AllureId("4006")
@DisplayName("Send (address screen): check address history")
@Test
fun sendAddressHistoryTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val recipientShortenedAddress = ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Click on previous address") {
onSendAddressScreen { recentAddressWithText(recipientShortenedAddress).clickWithAssertion() }
}
step("Assert recipient address is displayed") {
onSendConfirmScreen { recipientAddress(recipientAddress).assertIsDisplayed() }
}
step("Press system 'Back' button") {
device.uiDevice.pressBack()
}
step("Assert address text field contains correct recipient address") {
onSendAddressScreen { addressTextField.assertTextContains(recipientAddress) }
}
step("Assert 'Recent' title is displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsNotDisplayed() }
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Assert recipient text field is empty") {
onSendAddressScreen { addressTextField.assertTextEquals("") }
}
step("Assert 'Recent' title is displayed") {
onSendAddressScreen { recentAddressesTitle.assertIsDisplayed() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
@AllureId("4004")
@DisplayName("Send (address screen): check destination tag")
@Test
fun sendDestinationTagTest() {
val tokenName = "XRP Ledger"
val sendAmount = "1"
val correctMemo = "123"
val invalidMemo = "hz"
val xrpRecipientAddress = XRP_RECIPIENT_ADDRESS
val userTokensScenarioName = "user_tokens_api"
val userTokensScenarioState = "XRP"
val context = device.context
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(userTokensScenarioName)
}
).run {
step("Set WireMock scenario: '$userTokensScenarioName' to state: '$userTokensScenarioState'") {
setWireMockScenarioState(scenarioName = userTokensScenarioName, state = userTokensScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Set clipboard text") {
setClipboardText(context, correctMemo)
}
step("Assert 'Destination Tag' title is displayed") {
onSendAddressScreen { destinationTagBlockTitle().assertIsDisplayed() }
}
step("Assert 'Destination Tag' text is displayed") {
onSendAddressScreen { destinationTagBlockText.assertIsDisplayed() }
}
step("CLick on 'Paste' button") {
onSendAddressScreen { destinationTagPasteButton.clickWithAssertion() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(xrpRecipientAddress) }
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
step("Click on 'Clear text field button'") {
onSendAddressScreen { clearDestinationTagTextFieldButton.clickWithAssertion() }
}
step("Type invalid memo in input text field") {
onSendAddressScreen { destinationTagTextField.performTextReplacement(invalidMemo) }
}
step("Assert 'Invalid memo' title is displayed") {
onSendAddressScreen { destinationTagBlockTitle(isMemoCorrectOrEmpty = false).assertIsDisplayed() }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
@AllureId("4543")
@DisplayName("Send (address screen): check address field")
@Test
fun sendAddressFieldTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val recipientAddress = ETHEREUM_RECIPIENT_ADDRESS
val invalidAddress = "s"
val walletAddress = ETHEREUM_ADDRESS
val context = device.context
val recipient = getResourceString(R.string.send_recipient)
val notAValidAddress = getResourceString(R.string.send_recipient_address_error)
val sameAsWalletAddress = getResourceString(R.string.send_error_address_same_as_wallet)
setupHooks(
additionalAfterSection = {
clearClipboard()
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Set clipboard text") {
setClipboardText(context, recipientAddress)
}
step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
}
step("Assert address text field contains correct recipient address") {
onSendAddressScreen { addressTextField.assertTextContains(recipientAddress) }
}
step("Assert address text field title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(recipient) }
}
step("Assert 'Next' button is enabled") {
onSendAddressScreen { nextButton.assertIsEnabled() }
}
step("Set clipboard text") {
setClipboardText(context, invalidAddress)
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
}
step("Assert address text field contains invalid address") {
onSendAddressScreen { addressTextField.assertTextContains(invalidAddress) }
}
step("Assert invalid address text field title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
step("Set clipboard text") {
setClipboardText(context, walletAddress)
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Click on 'Paste' button") {
onSendAddressScreen { addressPasteButton.clickWithAssertion() }
}
step("Assert address text field contains invalid address") {
onSendAddressScreen { addressTextField.assertTextContains(walletAddress) }
}
step("Assert 'Address is the same as wallet address' error title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(sameAsWalletAddress) }
}
step("Assert 'Next' button is disabled") {
onSendAddressScreen { nextButton.assertIsNotEnabled() }
}
}
}
@AllureId("4004")
@DisplayName("Send (address screen): check ENS name")
@Test
fun sendEnsNameTest() {
val tokenName = "Ethereum"
val sendAmount = "1"
val ensName = ENS_NAME
val invalidEnsName = "l"
val ensAddress = ENS_ETHEREUM_RECIPIENT_ADDRESS
val ensShortenedAddress = ENS_ETHEREUM_RECIPIENT_SHORTENED_ADDRESS
val scenarioName = "eth_call_api"
val scenarioState = "EnsName"
val notAValidAddress = getResourceString(R.string.send_recipient_address_error)
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() }
}
step("Click on 'Send' button") {
onTokenDetailsScreen { sendButton().performClick() }
}
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type ENS name: '$ensName' in text field") {
onSendAddressScreen { addressTextField.performTextReplacement(ensName) }
}
step("Assert ENS address displayed") {
onSendAddressScreen { resolvedAddress.assertTextContains(ensAddress, substring = true) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert recipient address is displayed") {
onSendConfirmScreen { recipientAddress(ensName).assertIsDisplayed() }
}
step("Assert blockchain address is displayed") {
onSendConfirmScreen { blockchainAddress.assertTextContains(ensShortenedAddress, substring = true) }
}
step("Click on recipient address") {
onSendConfirmScreen { recipientAddress(ensName).performClick() }
}
step("Click on 'Cross' button") {
onSendAddressScreen { clearTextFieldButton.clickWithAssertion() }
}
step("Type invalid ENS name: '$invalidEnsName' in text field") {
onSendAddressScreen { addressTextField.performTextReplacement(invalidEnsName) }
}
step("Assert invalid address error title is displayed") {
onSendAddressScreen { addressTextFieldTitle.assertTextContains(notAValidAddress) }
}
}
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.AZERO_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import org.junit.Test
@HiltAndroidTest
class AzeroWarningsTest : BaseTestCase() {
private val tokenName = "Aleph Zero"
private val amountToLeaveLessThanDeposit = "0.099876587544"
private val amountToLeaveGreaterThanDeposit = "0.09"
private val depositAmount = "AZERO 0.0000000005"
private val warningTitleResId = R.string.send_notification_existential_deposit_title
private val warningMessageResId = R.string.send_notification_existential_deposit_text
@AllureId("4290")
@Test
fun checkWarning() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDeposit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(AZERO_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount
)
}
step("Click on 'Leave $depositAmount' button") {
onSendConfirmScreen { leaveDepositButton(depositAmount).clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount,
isDisplayed = false
)
}
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$amountToLeaveGreaterThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanDeposit)
}
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.KUSAMA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import org.junit.Test
@HiltAndroidTest
class KusamaWarningsTest : BaseTestCase() {
private val tokenName = "Kusama"
private val amountToLeaveLessThanDeposit = "0.300333"
private val amountToLeaveGreaterThanDeposit = "0.1"
private val depositAmount = "KSM 0.000333333333"
private val warningTitleResId = R.string.send_notification_existential_deposit_title
private val warningMessageResId = R.string.send_notification_existential_deposit_text
@AllureId("4291")
@Test
fun checkWarning() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDeposit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(KUSAMA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount
)
}
step("Click on 'Leave $depositAmount' button") {
onSendConfirmScreen { leaveDepositButton(depositAmount).clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount,
isDisplayed = false
)
}
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$amountToLeaveGreaterThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanDeposit)
}
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import org.junit.Test
@HiltAndroidTest
class PolkadotWarningsTest : BaseTestCase() {
private val tokenName = "Polkadot"
private val amountToLeaveLessThanDeposit = "1.299"
private val amountToLeaveGreaterThanDeposit = "0.2"
private val depositAmount = "DOT 1.00"
private val warningTitleResId = R.string.send_notification_existential_deposit_title
private val warningMessageResId = R.string.send_notification_existential_deposit_text
@AllureId("4289")
@Test
fun checkWarning() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanDeposit)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount
)
}
step("Click on 'Leave $depositAmount' button") {
onSendConfirmScreen { leaveDepositButton(depositAmount).clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount,
isDisplayed = false
)
}
step("Click on 'Amount' field") {
onSendConfirmScreen { primaryAmount.clickWithAssertion() }
}
step("Type '$amountToLeaveGreaterThanDeposit' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanDeposit)
}
}
step("Click on 'Continue' button") {
onSendScreen { continueButton.clickWithAssertion() }
}
step("Assert 'Existential deposit warning' is not displayed") {
checkSendWarning(
titleResId = warningTitleResId,
messageResId = warningMessageResId,
amount = depositAmount,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,183 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.SOLANA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class SolanaWarningsTest : BaseTestCase() {
private val tokenName = "Solana"
private val amountToLeaveLessThanRent = "0.0016941"
private val amountToLeaveGreaterThanRent = "0.0000941"
private val amountToLeaveRentOnly = "0.00168934"
private val rentAmount = "0.000890880"
private val invalidAmountTitleResId = R.string.send_notification_invalid_amount_title
private val invalidAmountMessageResId = R.string.send_notification_invalid_amount_rent_fee
@AllureId("564")
@DisplayName("Warnings: warning is displayed, if after send balance is less than rent amount (SOLANA)")
@Test
fun warningIsDisplayedWhenLeaveLessThanRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveLessThanRent' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveLessThanRent)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount
)
}
}
}
@AllureId("567")
@DisplayName("Warnings: warning is not displayed, if after send balance is greater than rent amount (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveGreaterThanRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveGreaterThanRent' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveGreaterThanRent)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount,
isDisplayed = false
)
}
}
}
@AllureId("566")
@DisplayName("Warnings: warning is not displayed, if after send balance is equal to rent amount (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveOnlyRent() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$amountToLeaveRentOnly' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(amountToLeaveRentOnly)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount,
isDisplayed = false
)
}
}
}
@AllureId("565")
@DisplayName("Warnings: warning is not displayed, if after send balance is zero (SOLANA)")
@Test
fun warningIsNotDisplayedWhenLeaveZeroSol() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type max amount in input text field") {
onSendScreen {
maxButton.performClick()
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(SOLANA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Invalid amount warning' is not displayed") {
checkSendWarning(
titleResId = invalidAmountTitleResId,
messageResId = invalidAmountMessageResId,
amount = rentAmount,
isDisplayed = false
)
}
}
}
}

View file

@ -0,0 +1,121 @@
package com.tangem.tests.send.warnings
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.TEZOS_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.scenarios.checkSendWarning
import com.tangem.scenarios.openSendScreen
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.wallet.R
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Test
@HiltAndroidTest
class TezosWarningsTest : BaseTestCase() {
private val tokenName = "Tezos"
private val reduceAmount = "0.000001"
private val sendAmount = "0.01"
private val feeIsHighTitleResId = R.string.send_notification_high_fee_title
private val feeIsHighMessageResId = R.string.send_notification_high_fee_text
@AllureId("4229")
@DisplayName("Warnings: warning is displayed when sending max amount")
@Test
fun warningIsDisplayedWhenSendMaxAmount() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Click 'Max amount' button") {
onSendScreen {
maxButton.clickWithAssertion()
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(TEZOS_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Swipe up to see the warning"){
swipeVertical(SwipeDirection.UP)
}
step("Assert 'Fee is high warning' is displayed") {
checkSendWarning(
titleResId = feeIsHighTitleResId,
messageResId = feeIsHighMessageResId,
amount = reduceAmount,
sendButtonIsDisabled = false
)
}
step("Click on 'Reduce' button") {
onSendConfirmScreen { reduceAmountButton(reduceAmount).clickWithAssertion() }
}
step("Assert 'Fee is high warning' is not displayed") {
checkSendWarning(
titleResId = feeIsHighTitleResId,
messageResId = feeIsHighMessageResId,
amount = reduceAmount,
isDisplayed = false
)
}
}
}
@AllureId("4230")
@DisplayName("Warnings: warning is not displayed when sending not max amount")
@Test
fun warningIsNotDisplayedWhenSendNotMaxAmount() {
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send Screen' with token: $tokenName") {
openSendScreen(tokenName)
}
step("Type '$sendAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(sendAmount)
}
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Type address in input text field") {
onSendAddressScreen { addressTextField.performTextReplacement(TEZOS_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert 'Fee is high warning' is not displayed") {
checkSendWarning(
titleResId = feeIsHighTitleResId,
messageResId = feeIsHighMessageResId,
amount = reduceAmount,
isDisplayed = false
)
}
}
}
}

@ -1 +1 @@
Subproject commit 4272136431c3629230803e70c4d2cf412365418d Subproject commit 7b792e100c14f64d3e44306c3a9b25a25d0cfc03

View file

@ -3,10 +3,13 @@ package com.tangem.tap.common.analytics
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.addHotWalletContext
import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.extensions.setHotWalletContext
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -17,6 +20,14 @@ internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
Analytics.setContext(scanResponse) Analytics.setContext(scanResponse)
} }
override fun addContext(userWallet: UserWallet) {
Analytics.addContext(userWallet)
}
override fun setHotWalletContext() {
Analytics.setHotWalletContext()
}
override fun eraseContext() { override fun eraseContext() {
Analytics.eraseContext() Analytics.eraseContext()
} }
@ -25,6 +36,10 @@ internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
Analytics.addContext(scanResponse) Analytics.addContext(scanResponse)
} }
override fun addHotWalletContext() {
Analytics.addHotWalletContext()
}
override fun removeContext() { override fun removeContext() {
Analytics.removeContext() Analytics.removeContext()
} }

View file

@ -6,6 +6,7 @@ class BlockchainApiExceptionEvent(
selectedHost: String, selectedHost: String,
exceptionHost: String, exceptionHost: String,
error: String, error: String,
blockchain: String,
) : AnalyticsEvent( ) : AnalyticsEvent(
category = "BlockchainSdk", category = "BlockchainSdk",
event = "Exception", event = "Exception",
@ -13,5 +14,6 @@ class BlockchainApiExceptionEvent(
AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost, AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost,
AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost, AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost,
AnalyticsParam.ERROR_DESCRIPTION to error, AnalyticsParam.ERROR_DESCRIPTION to error,
AnalyticsParam.BLOCKCHAIN to blockchain,
), ),
) )

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.analytics.handlers package com.tangem.tap.common.analytics.handlers
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.ExceptionHandlerOutput import com.tangem.blockchain.common.ExceptionHandlerOutput
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent
import javax.inject.Inject import javax.inject.Inject
@ -8,12 +10,13 @@ import javax.inject.Inject
class BlockchainExceptionHandler @Inject constructor( class BlockchainExceptionHandler @Inject constructor(
private val analyticsErrorHandler: AnalyticsErrorHandler, private val analyticsErrorHandler: AnalyticsErrorHandler,
) : ExceptionHandlerOutput { ) : ExceptionHandlerOutput {
override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { override fun handleApiSwitch(currentHost: String, nextHost: String, message: String, blockchain: Blockchain) {
analyticsErrorHandler.sendErrorEvent( analyticsErrorHandler.sendErrorEvent(
BlockchainApiExceptionEvent( BlockchainApiExceptionEvent(
selectedHost = nextHost, selectedHost = nextHost,
exceptionHost = currentHost, exceptionHost = currentHost,
error = message, error = message,
blockchain = blockchain.toNetworkId(),
), ),
) )
} }

View file

@ -0,0 +1,22 @@
package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
class HotWalletContextInterceptor(
val parent: ParamsInterceptor? = null,
) : ParamsInterceptor {
override fun id(): String = HotWalletContextInterceptor.id()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.PRODUCT_TYPE] = "Mobile Wallet"
}
companion object {
fun id(): String = HotWalletContextInterceptor::class.java.simpleName
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.domain.models.scan.ScanResponse
*/ */
class LinkedCardContextInterceptor( class LinkedCardContextInterceptor(
scanResponse: ScanResponse, scanResponse: ScanResponse,
val parent: LinkedCardContextInterceptor? = null, val parent: ParamsInterceptor? = null,
) : ParamsInterceptor { ) : ParamsInterceptor {
private val contextInterceptor = CardContextInterceptor(scanResponse) private val contextInterceptor = CardContextInterceptor(scanResponse)

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.paramsInterceptor.HotWalletContextInterceptor
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/** /**
@ -24,19 +25,46 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
fun Analytics.setContext(userWallet: UserWallet) { fun Analytics.setContext(userWallet: UserWallet) {
setUserId(userWallet.walletId.stringValue) setUserId(userWallet.walletId.stringValue)
// TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
if (userWallet is UserWallet.Cold) { when (userWallet) {
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse)) is UserWallet.Cold -> {
removeParamsInterceptor(HotWalletContextInterceptor.id())
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
}
is UserWallet.Hot -> {
removeParamsInterceptor(LinkedCardContextInterceptor.id())
addParamsInterceptor(HotWalletContextInterceptor())
}
} }
} }
fun Analytics.setHotWalletContext() {
addParamsInterceptor(HotWalletContextInterceptor())
}
/** /**
* Erases the context * Erases the context
*/ */
fun Analytics.eraseContext() { fun Analytics.eraseContext() {
clearUserId() clearUserId()
removeParamsInterceptor(LinkedCardContextInterceptor.id()) removeParamsInterceptor(LinkedCardContextInterceptor.id())
removeParamsInterceptor(HotWalletContextInterceptor.id())
}
/**
* Adds a new context and keeps a previous context as the parent of the new one
*/
fun Analytics.addContext(userWallet: UserWallet) {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = when (userWallet) {
is UserWallet.Cold -> LinkedCardContextInterceptor(userWallet.scanResponse, parent = currentContext)
is UserWallet.Hot -> HotWalletContextInterceptor(parent = currentContext)
}
setUserId(userWalletId = userWallet.walletId.stringValue)
addParamsInterceptor(newContext)
} }
/** /**
@ -48,18 +76,32 @@ fun Analytics.addContext(scanResponse: ScanResponse) {
setUserId(userWalletId.stringValue) setUserId(userWalletId.stringValue)
} }
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext) val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext)
addParamsInterceptor(newContext) addParamsInterceptor(newContext)
} }
fun Analytics.addHotWalletContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor
val newContext = HotWalletContextInterceptor(currentContext)
addParamsInterceptor(newContext)
}
/** /**
* Removes the current context and restores the previous one if it was present. * Removes the current context and restores the previous one if it was present.
*/ */
fun Analytics.removeContext() { fun Analytics.removeContext() {
val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id())
val previousContext = currentContext?.parent ?: return ?: removeParamsInterceptor(HotWalletContextInterceptor.id())
val previousContext = when (currentContext) {
is LinkedCardContextInterceptor -> currentContext.parent
is HotWalletContextInterceptor -> currentContext.parent
else -> null
} ?: return
addParamsInterceptor(previousContext) addParamsInterceptor(previousContext)
} }

View file

@ -1,23 +0,0 @@
package com.tangem.tap.common.extensions
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
// TODO: move extensions to utils
fun BigDecimal.toFormattedString(
decimals: Int,
roundingMode: RoundingMode = RoundingMode.DOWN,
locale: Locale = Locale.US,
): String {
val symbols = DecimalFormatSymbols(locale)
val df = DecimalFormat()
df.decimalFormatSymbols = symbols
df.maximumFractionDigits = decimals
df.minimumFractionDigits = 0
df.isGroupingUsed = true
df.roundingMode = roundingMode
return df.format(this)
}

View file

@ -2,8 +2,9 @@ package com.tangem.tap.data
import android.content.Context import android.content.Context
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -11,7 +12,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.text.encodeToByteArray
private const val DEFAULT_KEY = "tangem_pay_default_key" private const val DEFAULT_KEY = "tangem_pay_default_key"
private const val ORDER_ID_KEY = "tangem_pay_order_id_key" private const val ORDER_ID_KEY = "tangem_pay_order_id_key"
@ -19,6 +19,7 @@ private const val ORDER_ID_KEY = "tangem_pay_order_id_key"
@Singleton @Singleton
internal class DefaultTangemPayStorage @Inject constructor( internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context, @ApplicationContext applicationContext: Context,
@NetworkMoshi moshi: Moshi,
private val dispatcherProvider: CoroutineDispatcherProvider, private val dispatcherProvider: CoroutineDispatcherProvider,
) : TangemPayStorage { ) : TangemPayStorage {
@ -29,14 +30,21 @@ internal class DefaultTangemPayStorage @Inject constructor(
name = "tangem_pay_storage", name = "tangem_pay_storage",
) )
} }
private val moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) } private val tokensAdapter by lazy { moshi.adapter(VisaAuthTokens::class.java) }
override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) {
withContext(dispatcherProvider.io) {
secureStorage.store(key = createCustomerAddressKey(userWalletId), value = customerWalletAddress)
}
}
override suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String? {
return withContext(dispatcherProvider.io) {
secureStorage.getAsString(createCustomerAddressKey(userWalletId))
}
}
override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) = override suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) =
withContext(dispatcherProvider.io) { withContext(dispatcherProvider.io) {
val json = tokensAdapter.toJson(tokens) val json = tokensAdapter.toJson(tokens)
@ -71,10 +79,14 @@ internal class DefaultTangemPayStorage @Inject constructor(
secureStorage.delete(createOrderIdKey(customerWalletAddress)) secureStorage.delete(createOrderIdKey(customerWalletAddress))
} }
override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) { override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) =
secureStorage.delete(createKey(customerWalletAddress)) withContext(dispatcherProvider.io) {
secureStorage.delete(createOrderIdKey(customerWalletAddress)) secureStorage.delete(createCustomerAddressKey(userWalletId))
} secureStorage.delete(createKey(customerWalletAddress))
secureStorage.delete(createOrderIdKey(customerWalletAddress))
}
private fun createCustomerAddressKey(userWalletId: UserWalletId): String = userWalletId.stringValue
private fun createKey(address: String): String = "${DEFAULT_KEY}_$address" private fun createKey(address: String): String = "${DEFAULT_KEY}_$address"

View file

@ -1,17 +1,21 @@
package com.tangem.tap.di package com.tangem.tap.di
import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -46,17 +50,15 @@ internal object ActivityModule {
@Singleton @Singleton
fun provideDefaultRampManager( fun provideDefaultRampManager(
appStateHolder: AppStateHolder, appStateHolder: AppStateHolder,
expressServiceLoader: ExpressServiceLoader, expressServiceFetcher: ExpressServiceFetcher,
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
excludedBlockchains: ExcludedBlockchains,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): RampStateManager { ): RampStateManager {
return DefaultRampManager( return DefaultRampManager(
sellService = Provider { requireNotNull(appStateHolder.sellService) }, sellService = Provider { requireNotNull(appStateHolder.sellService) },
expressServiceLoader = expressServiceLoader, expressServiceFetcher = expressServiceFetcher,
currenciesRepository = currenciesRepository, currenciesRepository = currenciesRepository,
dispatchers = dispatchers, dispatchers = dispatchers,
excludedBlockchains = excludedBlockchains,
) )
} }
@ -67,6 +69,21 @@ internal object ActivityModule {
return CoroutineScope(SupervisorJob() + Dispatchers.IO) return CoroutineScope(SupervisorJob() + Dispatchers.IO)
} }
@Provides
@Singleton
fun provideExchangeService(
environmentConfigStorage: EnvironmentConfigStorage,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
moonPayApi: MoonPayApi,
): SellService {
return MoonPayService(
api = moonPayApi,
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey },
secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey },
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetPolkadotCheckHasResetUseCase( fun provideGetPolkadotCheckHasResetUseCase(

View file

@ -7,6 +7,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module import dagger.Module
@ -27,6 +28,7 @@ internal class TangemSdkManagerModule {
cardSdkConfigRepository: CardSdkConfigRepository, cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler, visaCardScanHandler: VisaCardScanHandler,
visaCardActivationTaskFactory: VisaCardActivationTask.Factory, visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
onboardingV2FeatureToggles: OnboardingV2FeatureToggles, onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
): TangemSdkManager { ): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) { return if (BuildConfig.MOCK_DATA_SOURCE) {
@ -37,6 +39,7 @@ internal class TangemSdkManagerModule {
resources = context.resources, resources = context.resources,
visaCardScanHandler = visaCardScanHandler, visaCardScanHandler = visaCardScanHandler,
visaCardActivationTaskFactory = visaCardActivationTaskFactory, visaCardActivationTaskFactory = visaCardActivationTaskFactory,
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingV2FeatureToggles = onboardingV2FeatureToggles,
) )
} }

View file

@ -3,6 +3,8 @@ package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.* import com.tangem.domain.account.usecase.*
import dagger.Module import dagger.Module
@ -50,10 +52,12 @@ internal object AccountDomainModule {
fun provideRecoverCryptoPortfolioUseCase( fun provideRecoverCryptoPortfolioUseCase(
accountsCRUDRepository: AccountsCRUDRepository, accountsCRUDRepository: AccountsCRUDRepository,
mainAccountTokensMigration: MainAccountTokensMigration, mainAccountTokensMigration: MainAccountTokensMigration,
cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher,
): RecoverCryptoPortfolioUseCase { ): RecoverCryptoPortfolioUseCase {
return RecoverCryptoPortfolioUseCase( return RecoverCryptoPortfolioUseCase(
crudRepository = accountsCRUDRepository, crudRepository = accountsCRUDRepository,
mainAccountTokensMigration = mainAccountTokensMigration, mainAccountTokensMigration = mainAccountTokensMigration,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
) )
} }

View file

@ -179,20 +179,6 @@ internal object OnrampDomainModule {
) )
} }
@Provides
@Singleton
fun provideGetOnrampV2QuotesUseCase(
settingsRepository: SettingsRepository,
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampV2QuotesUseCase {
return GetOnrampV2QuotesUseCase(
settingsRepository = settingsRepository,
repository = onrampRepository,
errorResolver = onrampErrorResolver,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetOnrampProviderWithQuoteUseCase( fun provideGetOnrampProviderWithQuoteUseCase(

View file

@ -20,11 +20,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
import com.tangem.domain.tokens.* import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.*
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
@ -60,24 +56,6 @@ internal object TokensDomainModule {
) )
} }
@Provides
@Singleton
fun provideFetchTokenListUseCase(
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchTokenListUseCase {
return FetchTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideFetchPendingTransactionsUseCase( fun provideFetchPendingTransactionsUseCase(
@ -184,24 +162,6 @@ internal object TokensDomainModule {
) )
} }
@Provides
@Singleton
fun provideFetchCardTokenListUseCase(
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
stakingIdFactory: StakingIdFactory,
): FetchCardTokenListUseCase {
return FetchCardTokenListUseCase(
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
stakingIdFactory = stakingIdFactory,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetCryptoCurrencyUseCase( fun provideGetCryptoCurrencyUseCase(

View file

@ -18,13 +18,17 @@ import com.tangem.core.res.getStringSafe
import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.* import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.operations.ScanTask import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
@ -44,6 +48,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask import com.tangem.tap.domain.tasks.visa.VisaCustomerWalletApproveTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
@ -62,6 +67,7 @@ internal class DefaultTangemSdkManager(
private val resources: Resources, private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler, private val visaCardScanHandler: VisaCardScanHandler,
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
) : TangemSdkManager { ) : TangemSdkManager {
@ -511,6 +517,18 @@ internal class DefaultTangemSdkManager(
) )
} }
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this),
cardId = cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
}
// endregion // endregion
companion object { companion object {

View file

@ -213,5 +213,11 @@ class MockTangemSdkManager(
error("Not implemented") error("Not implemented")
} }
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
error("Not implemented")
}
// endregion // endregion
} }

View file

@ -125,10 +125,18 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
), ),
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey( DerivationPath("m/44'/144'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
), ),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
), ),
extendedPublicKey = ExtendedPublicKey( extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
@ -235,6 +243,20 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/148'/0'") to ExtendedPublicKey( // XLM
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/1729'/0'/0'") to ExtendedPublicKey( // Tezos
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
), ),
), ),
ByteArrayKey( ByteArrayKey(
@ -244,14 +266,42 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap( ExtendedPublicKeysMap(
mapOf( mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey( // Solana
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/354'/0'/0'/0'") to ExtendedPublicKey( // Polkadot
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/434'/0'/0'/0'") to ExtendedPublicKey( // Kusama
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/643'/0'/0'/0'") to ExtendedPublicKey( // Azero
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
@ -296,9 +346,9 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // xrp DerivationPath("m/44'/1729'/0'/0'") to ExtendedPublicKey( // Tezos
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
@ -312,19 +362,47 @@ object WalletMockContent : MockContent {
ExtendedPublicKeysMap( ExtendedPublicKeysMap(
mapOf( mapOf(
DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano DerivationPath("m/1852'/1815'/0'/0/0") to ExtendedPublicKey( // cardano
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended DerivationPath("m/1852'/1815'/0'/2/0") to ExtendedPublicKey( // cardano extended
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0, depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/501'/0'") to ExtendedPublicKey( // Solana
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/354'/0'/0'/0'") to ExtendedPublicKey( // Polkadot
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/434'/0'/0'/0'") to ExtendedPublicKey( // Kusama
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/643'/0'/0'/0'") to ExtendedPublicKey( // Azero
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
), ),
), ),
), ),

View file

@ -17,7 +17,6 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
// TODO remove it after test after resolve [REDACTED_JIRA]
internal class ResetBackupCardTask( internal class ResetBackupCardTask(
private val userWalletId: UserWalletId, private val userWalletId: UserWalletId,
) : CardSessionRunnable<Boolean> { ) : CardSessionRunnable<Boolean> {

View file

@ -0,0 +1,130 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.domain.visa.model.sign
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val dispatchersProvider: CoroutineDispatcherProvider,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
) : CardSessionRunnable<TangemPayInitialCredentials> {
override fun run(session: CardSession, callback: CompletionCallback<TangemPayInitialCredentials>) {
coroutineScope.launch {
callback(runSuspend(session = session))
}
}
private suspend fun runSuspend(session: CardSession): CompletionResult<TangemPayInitialCredentials> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)
val derivationResult = runDerivationTask(session, wallet)
val address = when (derivationResult) {
is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error)
is CompletionResult.Success<ExtendedPublicKey> -> generateAddressFromExtendedKey(derivationResult.data)
}
val challenge = withContext(dispatchersProvider.io) {
visaAuthRemoteDataSource.getCustomerWalletAuthChallenge(address)
}.getOrElse { return CompletionResult.Failure(it.tangemError) }
val dataToSign = VisaDataToSignByCustomerWallet(hashToSign = challenge.challenge)
val approveResult = runVisaCustomerWalletApproveTask(
session = session,
cardId = card.cardId,
targetAddress = address,
dataToSign = dataToSign,
)
val signedData = when (approveResult) {
is CompletionResult.Failure<*> -> return CompletionResult.Failure(approveResult.error)
is CompletionResult.Success<VisaSignedDataByCustomerWallet> -> approveResult.data
}
val authTokens = withContext(dispatchersProvider.io) {
visaAuthRemoteDataSource.getTokenWithCustomerWallet(
sessionId = challenge.session.sessionId,
signature = signedData.signature,
nonce = signedData.dataToSign.hashToSign,
)
}.getOrNull() ?: return CompletionResult.Failure(VisaActivationError.FailedRemoteState.tangemError)
return CompletionResult.Success(
data = TangemPayInitialCredentials(
customerWalletAddress = address,
authTokens = authTokens,
),
)
}
private suspend fun runDerivationTask(
session: CardSession,
wallet: CardWallet,
): CompletionResult<ExtendedPublicKey> {
val deferred = CompletableDeferred<CompletionResult<ExtendedPublicKey>>()
val derivationTask = DeriveWalletPublicKeyTask(
walletPublicKey = wallet.publicKey,
derivationPath = VisaUtilities.customDerivationPath,
)
derivationTask.run(session = session, callback = deferred::complete)
return deferred.await()
}
private suspend fun runVisaCustomerWalletApproveTask(
session: CardSession,
cardId: String,
targetAddress: String,
dataToSign: VisaDataToSignByCustomerWallet,
): CompletionResult<VisaSignedDataByCustomerWallet> {
val deferred = CompletableDeferred<CompletionResult<VisaSignedDataByCustomerWallet>>()
val task = VisaCustomerWalletApproveTask(
visaDataForApprove = VisaCustomerWalletApproveTask.Input(
cardId = cardId,
targetAddress = targetAddress,
hashToSign = dataToSign.hashToSign,
sign = dataToSign::sign,
),
)
task.run(session = session, callback = deferred::complete)
return deferred.await()
}
private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String {
val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey(
extendedPublicKey = extendedPublicKey,
cachedIndex = null,
)
return derivationData.address
}
@AssistedFactory
interface Factory {
fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
@ -59,21 +58,7 @@ class VisaCustomerWalletApproveTask(
session: CardSession, session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>, callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) { ) {
val cardDTO = CardDTO(card) val derivationPath = VisaUtilities.customDerivationPath
val derivationStyle = cardDTO.derivationStyleProvider.getDerivationStyle() ?: run {
proceedApproveWithLegacyCard(
card = card,
session = session,
callback = callback,
)
return
}
val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run {
callback(CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError))
return
}
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError))

View file

@ -16,14 +16,11 @@ import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.SendPushTokenUseCase
@ -42,8 +39,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -83,9 +79,9 @@ internal class MainViewModel @Inject constructor(
private val apiConfigsManager: ApiConfigsManager, private val apiConfigsManager: ApiConfigsManager,
private val multiQuoteUpdater: MultiQuoteUpdater, private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder, private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig, private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() { ) : ViewModel() {
@ -195,22 +191,12 @@ internal class MainViewModel @Inject constructor(
private fun initializeOffRamp() { private fun initializeOffRamp() {
viewModelScope.launch { viewModelScope.launch {
val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync())
appStateHolder.sellService = sellService appStateHolder.sellService = sellService
sellService.update() sellService.update()
} }
} }
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
isLogEnabled = LogConfig.network.moonPayService,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
private fun observeFlips() { private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope) listenToFlipsUseCase().launchIn(viewModelScope)
} }

View file

@ -2,44 +2,14 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.converter.Converter
import com.tangem.tap.store
import com.tangem.utils.converter.TwoWayConverter
internal class CryptoCurrencyConverter( internal object CryptoCurrencyConverter : Converter<CryptoCurrency, Currency> {
private val excludedBlockchains: ExcludedBlockchains,
) : TwoWayConverter<Currency, CryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } override fun convert(value: CryptoCurrency): Currency {
override fun convert(value: Currency): CryptoCurrency {
return when (value) {
is Currency.Blockchain -> requireNotNull(
cryptoCurrencyFactory.createCoin(
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = getSelectedWallet(),
),
)
is Currency.Token -> requireNotNull(
cryptoCurrencyFactory.createToken(
sdkToken = value.token,
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = getSelectedWallet(),
),
)
}
}
override fun convertBack(value: CryptoCurrency): Currency {
val blockchain = value.network.toBlockchain() val blockchain = value.network.toBlockchain()
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) { return when (value) {
@ -60,15 +30,4 @@ internal class CryptoCurrencyConverter(
) )
} }
} }
fun getSelectedWallet(): UserWallet {
val userWalletListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
requireNotNull(userWalletsListRepository.selectedUserWallet.value)
} else {
requireNotNull(userWalletListManager.selectedUserWalletSync)
}
}
} }

View file

@ -5,13 +5,11 @@ import arrow.core.raise.catch
import arrow.core.raise.either import arrow.core.raise.either
import arrow.core.raise.ensure import arrow.core.raise.ensure
import arrow.core.right import arrow.core.right
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.Lce
import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.ExpressAvailabilityState
import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.express.ExpressServiceFetcher
import com.tangem.domain.express.models.ExpressAsset
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
@ -26,17 +24,13 @@ import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
@Suppress("LongParameterList")
internal class DefaultRampManager( internal class DefaultRampManager(
private val sellService: Provider<ExchangeService>, private val sellService: Provider<SellService>,
private val expressServiceLoader: ExpressServiceLoader, private val expressServiceFetcher: ExpressServiceFetcher,
private val currenciesRepository: CurrenciesRepository, private val currenciesRepository: CurrenciesRepository,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
excludedBlockchains: ExcludedBlockchains,
) : RampStateManager { ) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains)
override suspend fun availableForBuy( override suspend fun availableForBuy(
userWallet: UserWallet, userWallet: UserWallet,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
@ -56,7 +50,7 @@ internal class DefaultRampManager(
return either { return either {
val isSellSupportedByService = catch( val isSellSupportedByService = catch(
block = { block = {
val serviceCurrency = cryptoCurrencyConverter.convertBack(status.currency) val serviceCurrency = CryptoCurrencyConverter.convert(status.currency)
sellService().availableForSell(currency = serviceCurrency) sellService().availableForSell(currency = serviceCurrency)
}, },
@ -96,7 +90,7 @@ internal class DefaultRampManager(
return availabilityState.toReason(cryptoCurrency.name) return availabilityState.toReason(cryptoCurrency.name)
} }
override fun getSellInitializationStatus(): Flow<ExchangeServiceInitializationStatus> { override fun getSellInitializationStatus(): Flow<SellServiceInitializationStatus> {
return sellService.invoke().initializationStatus return sellService.invoke().initializationStatus
} }
@ -106,8 +100,8 @@ internal class DefaultRampManager(
} }
} }
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> { override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<SellServiceInitializationStatus> {
return expressServiceLoader.getInitializationStatus(userWalletId) return expressServiceFetcher.getInitializationStatus(userWalletId)
} }
override suspend fun getSendUnavailabilityReason( override suspend fun getSendUnavailabilityReason(
@ -151,14 +145,14 @@ internal class DefaultRampManager(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState { ): ExpressAvailabilityState {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading ?: return ExpressAvailabilityState.Loading
return when (asset) { return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> { is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) }
foundAsset?.exchangeAvailable?.toSwapAvailabilityState() foundAsset?.isExchangeAvailable?.toSwapAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound ?: ExpressAvailabilityState.AssetNotFound
} }
} }
@ -168,15 +162,15 @@ internal class DefaultRampManager(
userWalletId: UserWalletId, userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency, cryptoCurrency: CryptoCurrency,
): ExpressAvailabilityState { ): ExpressAvailabilityState {
val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull()
?: return ExpressAvailabilityState.Loading ?: return ExpressAvailabilityState.Loading
return when (asset) { return when (asset) {
is Lce.Error -> ExpressAvailabilityState.Error is Lce.Error -> ExpressAvailabilityState.Error
is Lce.Loading -> ExpressAvailabilityState.Loading is Lce.Loading -> ExpressAvailabilityState.Loading
is Lce.Content -> { is Lce.Content -> {
val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) }
foundAsset?.onrampAvailable?.toOnrampAvailabilityState() foundAsset?.isOnrampAvailable?.toOnrampAvailabilityState()
?: ExpressAvailabilityState.AssetNotFound ?: ExpressAvailabilityState.AssetNotFound
} }
} }
@ -211,8 +205,13 @@ internal class DefaultRampManager(
} }
} }
private fun CryptoCurrency.findAssetPredicate(asset: Asset): Boolean { private fun CryptoCurrency.findAssetPredicate(assetId: ExpressAsset.ID): Boolean {
val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE val currencyAssedId = ExpressAsset.ID(
return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true) networkId = this.network.backendId,
contractAddress = (this as? CryptoCurrency.Token)?.contractAddress,
)
return assetId.networkId == currencyAssedId.networkId &&
assetId.contractAddress.equals(currencyAssedId.contractAddress, ignoreCase = true)
} }
} }

View file

@ -5,11 +5,11 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
typealias ExchangeServiceInitializationStatus = Lce<Throwable, Any> typealias SellServiceInitializationStatus = Lce<Throwable, Any>
interface ExchangeService { interface SellService {
val initializationStatus: StateFlow<ExchangeServiceInitializationStatus> val initializationStatus: StateFlow<SellServiceInitializationStatus>
suspend fun update() suspend fun update()

View file

@ -5,7 +5,9 @@ import android.util.Base64
import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.services.Result import com.tangem.common.services.Result
import com.tangem.common.services.performRequest import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.moonpay.MoonPayCurrencies
import com.tangem.datasource.api.moonpay.MoonPayUserStatus
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withIOContext import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceContent
@ -14,9 +16,10 @@ import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber import timber.log.Timber
@ -24,25 +27,18 @@ import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
class MoonPayService( class MoonPayService(
private val apiKey: String, private val api: MoonPayApi,
private val secretKey: String, private val apiKeyProvider: Provider<String>,
private val isLogEnabled: Boolean, private val secretKeyProvider: Provider<String>,
private val userWalletProvider: () -> UserWallet?, private val userWalletProvider: () -> UserWallet?,
) : ExchangeService { ) : SellService {
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus> override val initializationStatus: StateFlow<SellServiceInitializationStatus>
get() = _initializationStatus get() = _initializationStatus
private val _initializationStatus: MutableStateFlow<ExchangeServiceInitializationStatus> = private val _initializationStatus: MutableStateFlow<SellServiceInitializationStatus> =
MutableStateFlow(value = lceLoading()) MutableStateFlow(value = lceLoading())
private val api: MoonPayApi by lazy {
createRetrofitInstance(
baseUrl = MoonPayApi.MOOONPAY_BASE_URL,
logEnabled = isLogEnabled,
).create(MoonPayApi::class.java)
}
private var status: MoonPayStatus? = null private var status: MoonPayStatus? = null
override suspend fun update() { override suspend fun update() {
@ -51,7 +47,7 @@ class MoonPayService(
_initializationStatus.value = lceLoading() _initializationStatus.value = lceLoading()
performRequest { performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) {
is Result.Failure -> { is Result.Failure -> {
Timber.e("Failed to load user status", result.error) Timber.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError() _initializationStatus.value = result.error.lceError()
@ -60,7 +56,7 @@ class MoonPayService(
is Result.Success -> result.data is Result.Success -> result.data
} }
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) {
is Result.Failure -> { is Result.Failure -> {
Timber.e("Failed to load currencies", result.error) Timber.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError() _initializationStatus.value = result.error.lceError()
@ -78,7 +74,7 @@ class MoonPayService(
MoonPayAvailableCurrency( MoonPayAvailableCurrency(
currencyCode = currency.code, currencyCode = currency.code,
networkCode = currency.metadata?.networkCode ?: return@mapNotNull null, networkCode = currency.metadata?.networkCode ?: return@mapNotNull null,
contractAddress = currency.metadata.contractAddress, contractAddress = currency.metadata?.contractAddress,
) )
} }
@ -167,7 +163,7 @@ class MoonPayService(
val uri = Uri.Builder() val uri = Uri.Builder()
.scheme(SCHEME) .scheme(SCHEME)
.authority(URL_SELL) .authority(URL_SELL)
.appendQueryParameter("apiKey", apiKey) .appendQueryParameter("apiKey", apiKeyProvider())
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase()) .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}") .appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
@ -191,7 +187,7 @@ class MoonPayService(
private fun createSignature(data: String): String { private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256") val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256") val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey) sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray()) val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)

View file

@ -160,6 +160,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Pepecoin, PepecoinTestnet -> null Pepecoin, PepecoinTestnet -> null
Hyperliquid, HyperliquidTestnet -> null Hyperliquid, HyperliquidTestnet -> null
Quai, QuaiTestnet -> null Quai, QuaiTestnet -> null
// Linea, LineaTestnet -> null Linea, LineaTestnet -> null
// ArbitrumNova -> null ArbitrumNova -> null
} }

View file

@ -7,7 +7,7 @@ import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.SellService
import org.rekotlin.Action import org.rekotlin.Action
import org.rekotlin.Store import org.rekotlin.Store
import javax.inject.Inject import javax.inject.Inject
@ -19,7 +19,7 @@ import javax.inject.Inject
class AppStateHolder @Inject constructor() : ReduxStateHolder { class AppStateHolder @Inject constructor() : ReduxStateHolder {
var mainStore: Store<AppState>? = null var mainStore: Store<AppState>? = null
var sellService: ExchangeService? = null var sellService: SellService? = null
override fun dispatch(action: Action) { override fun dispatch(action: Action) {
mainStore?.dispatch(action) mainStore?.dispatch(action)

View file

@ -36,9 +36,10 @@ import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.SwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.* import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
@ -62,6 +63,7 @@ internal class ChildFactory @Inject constructor(
private val detailsComponentFactory: DetailsComponent.Factory, private val detailsComponentFactory: DetailsComponent.Factory,
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val walletBackupComponentFactory: WalletBackupComponent.Factory, private val walletBackupComponentFactory: WalletBackupComponent.Factory,
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
@ -100,6 +102,7 @@ internal class ChildFactory @Inject constructor(
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory, private val createWalletStartComponentFactory: CreateWalletStartComponent.Factory,
private val createHardwareWalletComponentFactory: CreateHardwareWalletComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory, private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
@ -107,8 +110,9 @@ internal class ChildFactory @Inject constructor(
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val viewPhraseComponentFactory: ViewPhraseComponent.Factory, private val viewPhraseComponentFactory: ViewPhraseComponent.Factory,
private val forgetWalletComponentFactory: ForgetWalletComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory, private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory, private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
@ -138,7 +142,6 @@ internal class ChildFactory @Inject constructor(
is AppRoute.ManageTokens -> { is AppRoute.ManageTokens -> {
val source = when (route.source) { val source = when (route.source) {
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
} }
@ -187,6 +190,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletBackupComponentFactory, componentFactory = walletBackupComponentFactory,
) )
} }
is AppRoute.WalletHardwareBackup -> {
createComponentChild(
context = context,
params = WalletHardwareBackupComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = walletHardwareBackupComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> { is AppRoute.MarketsTokenDetails -> {
createComponentChild( createComponentChild(
context = context, context = context,
@ -293,7 +305,7 @@ internal class ChildFactory @Inject constructor(
context = context, context = context,
params = StakingComponent.Params( params = StakingComponent.Params(
userWalletId = route.userWalletId, userWalletId = route.userWalletId,
cryptoCurrencyId = route.cryptoCurrencyId, cryptoCurrency = route.cryptoCurrency,
yieldId = route.yieldId, yieldId = route.yieldId,
), ),
componentFactory = stakingComponentFactory, componentFactory = stakingComponentFactory,
@ -496,6 +508,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = createWalletSelectionComponentFactory, componentFactory = createWalletSelectionComponentFactory,
) )
} }
is AppRoute.CreateHardwareWallet -> {
createComponentChild(
context = context,
params = Unit,
componentFactory = createHardwareWalletComponentFactory,
)
}
is AppRoute.CreateMobileWallet -> { is AppRoute.CreateMobileWallet -> {
createComponentChild( createComponentChild(
context = context, context = context,
@ -533,6 +552,7 @@ internal class ChildFactory @Inject constructor(
context = context, context = context,
params = CreateWalletBackupComponent.Params( params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId, userWalletId = route.userWalletId,
isUpgradeFlow = route.isUpgradeFlow,
), ),
componentFactory = createWalletBackupComponentFactory, componentFactory = createWalletBackupComponentFactory,
) )
@ -555,6 +575,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = viewPhraseComponentFactory, componentFactory = viewPhraseComponentFactory,
) )
} }
is AppRoute.ForgetWallet -> {
createComponentChild(
context = context,
params = ForgetWalletComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = forgetWalletComponentFactory,
)
}
is AppRoute.SendEntryPoint -> { is AppRoute.SendEntryPoint -> {
createComponentChild( createComponentChild(
context = context, context = context,
@ -604,8 +633,11 @@ internal class ChildFactory @Inject constructor(
is AppRoute.TangemPayDetails -> { is AppRoute.TangemPayDetails -> {
createComponentChild( createComponentChild(
context = context, context = context,
params = TangemPayDetailsComponent.Params(config = route.config), params = TangemPayDetailsContainerComponent.Params(
componentFactory = tangemPayDetailsComponentFactory, userWalletId = route.userWalletId,
config = route.config,
),
componentFactory = tangemPayDetailsContainerComponentFactory,
) )
} }
is AppRoute.TangemPayOnboarding -> { is AppRoute.TangemPayOnboarding -> {

View file

@ -34,18 +34,36 @@ interface Injected {
// Test Logging // Test Logging
subprojects { subprojects {
tasks.withType<Test> { tasks.withType<Test>().configureEach {
val taskName = name.lowercase()
if (taskName.contains("external") ||
taskName.contains("internal") ||
taskName.contains("release") ||
taskName.contains("mocked") ||
taskName.contains("huawei")
) {
enabled = false
println("Skipping test task: $name")
} else {
println("Test task scheduled: $name")
}
testLogging { testLogging {
exceptionFormat = TestExceptionFormat.FULL exceptionFormat = TestExceptionFormat.FULL
showStandardStreams = true showStandardStreams = true
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result -> afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
if (desc.parent == null) { // will match the outermost suite if (desc.parent == null) { // will match the outermost suite
val output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" val output =
"Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
val startItem = "| " val startItem = "| "
val endItem = " |" val endItem = " |"
val repeatLength = startItem.length + output.length + endItem.length val repeatLength = startItem.length + output.length + endItem.length
println("\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(repeatLength)) println(
"\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(
repeatLength
)
)
} }
})) }))
} }

View file

@ -126,9 +126,12 @@ sealed class AppRoute(val path: String) : Route {
val portfolioId: PortfolioId? = null, val portfolioId: PortfolioId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") { ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
/**
* Source of launching the screen.
* ManageTokens screen launched from Onboarding by another route. See `OnboardingRoute.ManageTokens`.
*/
enum class Source { enum class Source {
STORIES, STORIES,
ONBOARDING,
SETTINGS, SETTINGS,
} }
} }
@ -192,9 +195,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class Staking( data class Staking(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val cryptoCurrencyId: CryptoCurrency.ID, val cryptoCurrency: CryptoCurrency,
val yieldId: String, val yieldId: String,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId") ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
@Serializable @Serializable
data class PushNotification( data class PushNotification(
@ -217,6 +220,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}") ) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}")
@Serializable
data class WalletHardwareBackup(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_hardware_backup/${userWalletId.stringValue}")
@Serializable @Serializable
data object Markets : AppRoute(path = "/markets") data object Markets : AppRoute(path = "/markets")
@ -322,6 +330,9 @@ sealed class AppRoute(val path: String) : Route {
} }
} }
@Serializable
object CreateHardwareWallet : AppRoute(path = "/create_hardware_wallet")
@Serializable @Serializable
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")
@ -341,6 +352,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class CreateWalletBackup( data class CreateWalletBackup(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val isUpgradeFlow: Boolean,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable @Serializable
@ -353,6 +365,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}") ) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}")
@Serializable
data class ForgetWallet(
val userWalletId: UserWalletId,
) : AppRoute(path = "/forget_wallet/${userWalletId.stringValue}")
@Serializable @Serializable
data class SendEntryPoint( data class SendEntryPoint(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
@ -383,8 +400,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class TangemPayDetails( data class TangemPayDetails(
val userWalletId: UserWalletId,
val config: TangemPayDetailsConfig, val config: TangemPayDetailsConfig,
) : AppRoute(path = "/tangem_pay_details") ) : AppRoute(path = "/tangem_pay_details/${userWalletId.stringValue}")
@Serializable @Serializable
data class TangemPayOnboarding( data class TangemPayOnboarding(

View file

@ -1,21 +1,30 @@
package com.tangem.common.ui.account package com.tangem.common.ui.account
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.quote.PriceChange
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
class AccountCryptoPortfolioItemStateConverter( class AccountCryptoPortfolioItemStateConverter(
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val account: Account.CryptoPortfolio, private val account: Account.CryptoPortfolio,
private val priceChangeLce: Lce<Unit, PriceChange>? = null,
private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null, private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null,
private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null, private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null,
) : Converter<TotalFiatBalance, TokenItemState> { ) : Converter<TotalFiatBalance, TokenItemState> {
@ -31,6 +40,14 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToContentState( private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded, fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content { ): TokenItemState.Content {
val subtitle2State = when (fiatBalance.amount.isZero()) {
true -> null
false -> priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
return TokenItemState.Content( return TokenItemState.Content(
id = account.accountId.value, id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(this), iconState = AccountIconItemStateConverter.convert(this),
@ -50,14 +67,14 @@ class AccountCryptoPortfolioItemStateConverter(
.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, .format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
isFlickering = fiatBalance.source == StatusSource.CACHE, isFlickering = fiatBalance.source == StatusSource.CACHE,
), ),
subtitle2State = null, subtitle2State = subtitle2State,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } }, onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } }, onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } },
) )
} }
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Loading { private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
return TokenItemState.Loading( return TokenItemState.Content(
id = account.accountId.value, id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(account), iconState = AccountIconItemStateConverter.convert(account),
titleState = TokenItemState.TitleState.Content( titleState = TokenItemState.TitleState.Content(
@ -71,6 +88,10 @@ class AccountCryptoPortfolioItemStateConverter(
), ),
isAvailable = false, isAvailable = false,
), ),
fiatAmountState = FiatAmountState.Loading,
subtitle2State = Subtitle2State.Loading,
onItemLongClick = null,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
) )
} }
@ -97,4 +118,14 @@ class AccountCryptoPortfolioItemStateConverter(
}, },
) )
} }
private fun BigDecimal.getPriceChangeType(): PriceChangeType = PriceChangeConverter.fromBigDecimal(value = this)
private fun StatusSource.isFlickering(): Boolean = this == StatusSource.CACHE
private fun PriceChange.toSubtitle2State(): Subtitle2State = Subtitle2State.PriceChangeContent(
priceChangePercent = this.value.format { percent() },
type = this.value.getPriceChangeType(),
isFlickering = this.source.isFlickering(),
)
} }

View file

@ -33,7 +33,7 @@ sealed interface AccountNameUM {
* *
* @property raw the raw string value of the custom account name * @property raw the raw string value of the custom account name
*/ */
class Custom(internal val raw: String) : AccountNameUM { data class Custom(internal val raw: String) : AccountNameUM {
override val value: TextReference = stringReference(value = raw) override val value: TextReference = stringReference(value = raw)
} }

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -29,6 +30,7 @@ fun AccountTitle(
accountTitleUM: AccountTitleUM, accountTitleUM: AccountTitleUM,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.subtitle2, textStyle: TextStyle = TangemTheme.typography.subtitle2,
textColor: Color = TangemTheme.colors.text.tertiary,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -40,19 +42,20 @@ fun AccountTitle(
Text( Text(
text = accountTitleUM.prefixText.resolveReference(), text = accountTitleUM.prefixText.resolveReference(),
style = textStyle, style = textStyle,
color = TangemTheme.colors.text.tertiary, color = textColor,
) )
AccountLabel( AccountLabel(
name = accountTitleUM.name, name = accountTitleUM.name,
icon = accountTitleUM.icon, icon = accountTitleUM.icon,
iconSize = AccountIconSize.ExtraSmall, iconSize = AccountIconSize.ExtraSmall,
nameStyle = textStyle, nameStyle = textStyle,
nameColor = textColor,
) )
} }
is AccountTitleUM.Text -> Text( is AccountTitleUM.Text -> Text(
text = accountTitleUM.title.resolveReference(), text = accountTitleUM.title.resolveReference(),
style = textStyle, style = textStyle,
color = TangemTheme.colors.text.tertiary, color = textColor,
modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE), modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE),
) )
} }

View file

@ -0,0 +1,127 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.AccountName
@Composable
fun PortfolioSelectRow(
state: PortfolioSelectUM,
modifier: Modifier = Modifier,
leftContent: @Composable RowScope.() -> Unit = {},
) {
Row(
modifier = modifier
.clickable(enabled = state.isMultiChoice, onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
leftContent()
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(leftText),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW12()
if (state.icon != null) {
AccountIcon(
name = state.name,
icon = state.icon,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = state.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (state.isMultiChoice) {
Icon(
modifier = Modifier
.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Immutable
data class PortfolioSelectUM(
val icon: CryptoPortfolioIconUM?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,
val onClick: () -> Unit,
)
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PortfolioSelectRowPreview(@PreviewParameter(PreviewProvider::class) state: PortfolioSelectUM) {
TangemThemePreview {
PortfolioSelectRow(
state = state,
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
)
}
}
private class PreviewProvider : PreviewParameterProvider<PortfolioSelectUM> {
override val values: Sequence<PortfolioSelectUM>
get() = sequenceOf(PortfolioSelectRowPreviewData.account, PortfolioSelectRowPreviewData.wallet)
}
object PortfolioSelectRowPreviewData {
val account
get() = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
val wallet
get() = PortfolioSelectUM(
icon = null,
name = stringReference("Wallet Name"),
isMultiChoice = false,
isAccountMode = false,
onClick = {},
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.core.analytics.utils package com.tangem.core.analytics.utils
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -9,9 +10,15 @@ interface AnalyticsContextProxy {
fun setContext(scanResponse: ScanResponse) fun setContext(scanResponse: ScanResponse)
fun addContext(userWallet: UserWallet)
fun setHotWalletContext()
fun eraseContext() fun eraseContext()
fun addContext(scanResponse: ScanResponse) fun addContext(scanResponse: ScanResponse)
fun addHotWalletContext()
fun removeContext() fun removeContext()
} }

View file

@ -30,9 +30,5 @@
{ {
"name": "zklink", "name": "zklink",
"version": "undefined" "version": "undefined"
},
{
"name": "scroll",
"version": "undefined"
} }
] ]

View file

@ -27,6 +27,7 @@ sealed class ApiConfig {
TangemPay, TangemPay,
BlockAid, BlockAid,
YieldSupply, YieldSupply,
MoonPay,
} }
private fun initializeId(): ID { private fun initializeId(): ID {
@ -37,6 +38,7 @@ sealed class ApiConfig {
is TangemPay -> ID.TangemPay is TangemPay -> ID.TangemPay
is BlockAid -> ID.BlockAid is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
} }
} }

View file

@ -0,0 +1,43 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
/**
* MoonPay [ApiConfig]
*/
internal class MoonPay : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
)
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.datasource.api.express.models
object TangemExpressValues {
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.network.exchangeServices.moonpay package com.tangem.datasource.api.moonpay
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -7,17 +7,11 @@ import retrofit2.http.Query
interface MoonPayApi { interface MoonPayApi {
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL) @GET("v4/ip_address/")
suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL) @GET("v3/currencies/")
suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies> suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
} }
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -7,6 +7,7 @@ import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
@ -146,4 +147,10 @@ interface TangemPayApi {
@Header("Authorization") authHeader: String, @Header("Authorization") authHeader: String,
@Body body: CardDetailsRequest, @Body body: CardDetailsRequest,
): ApiResponse<CardDetailsResponse> ): ApiResponse<CardDetailsResponse>
@PUT("v1/customer/card/pin")
suspend fun setPin(
@Header("Authorization") authHeader: String,
@Body body: SetPinRequest,
): ApiResponse<SetPinResponse>
} }

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SetPinRequest(
@Json(name = "pin") val pin: String,
@Json(name = "session_id") val sessionId: String,
@Json(name = "iv") val iv: String,
)

View file

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

View file

@ -20,4 +20,18 @@ data class GetWalletAccountsResponse(
@Json(name = "sort") val sort: SortType, @Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int, @Json(name = "totalAccounts") val totalAccounts: Int,
) )
}
/** Flattens the tokens from all wallet accounts into a single list */
fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
return accounts.flatMap { it.tokens.orEmpty() }
}
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
group = wallet.group,
sort = wallet.sort,
tokens = flattenTokens(),
)
} }

View file

@ -76,4 +76,10 @@ internal object ApiConfigsModule {
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage) return BlockAid(environmentConfigStorage)
} }
@Provides
@IntoSet
fun provideMoonPayConfig(): ApiConfig {
return MoonPay()
}
} }

View file

@ -5,12 +5,14 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.MoonPay
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.StakeKitApi
@ -130,4 +132,13 @@ internal object NetworkModule {
applyTimeoutAnnotations = false, applyTimeoutAnnotations = false,
) )
} }
@Provides
@Singleton
fun provideMoonPayApi(retrofitApiBuilder: RetrofitApiBuilder): MoonPayApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.MoonPay,
applyTimeoutAnnotations = false,
)
}
} }

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di.exchangeservice
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ExchangeServiceLoaderModule {
@Binds
@Singleton
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
}

View file

@ -197,6 +197,7 @@ internal class RetrofitApiBuilder @Inject constructor(
val excludedApiForLogging: Set<ApiConfig.ID> = setOf( val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit, // ApiConfig.ID.StakeKit,
ApiConfig.ID.MoonPay,
) )
} }
} }

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.exchangeservice.swap
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Express service loader
*
[REDACTED_AUTHOR]
*/
interface ExpressServiceLoader {
/** Update service using [userWallet] and [userTokens] */
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>
}

View file

@ -1,9 +1,13 @@
package com.tangem.datasource.local.visa package com.tangem.datasource.local.visa
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.VisaAuthTokens import com.tangem.domain.visa.model.VisaAuthTokens
interface TangemPayStorage { interface TangemPayStorage {
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens) suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens? suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
@ -14,5 +18,5 @@ interface TangemPayStorage {
suspend fun clearOrderId(customerWalletAddress: String) suspend fun clearOrderId(customerWalletAddress: String)
suspend fun clearAll(customerWalletAddress: String) suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
} }

View file

@ -56,6 +56,7 @@ class ApiConfigTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk()) ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk()) ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk()) ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
ApiConfig.ID.MoonPay -> MoonPay()
} }
} }
} }

View file

@ -103,6 +103,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider) ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider) ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage) ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
ApiConfig.ID.MoonPay -> MoonPay()
} }
} }
} }
@ -115,6 +116,7 @@ internal class ProdApiConfigsManagerTest {
ApiConfig.ID.StakeKit -> createStakeKitModel() ApiConfig.ID.StakeKit -> createStakeKitModel()
ApiConfig.ID.TangemPay -> createTangemPayModel() ApiConfig.ID.TangemPay -> createTangemPayModel()
ApiConfig.ID.BlockAid -> createBlockAidSdkModel() ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
ApiConfig.ID.MoonPay -> createMoonPayModel()
} }
} }
@ -257,6 +259,16 @@ internal class ProdApiConfigsManagerTest {
) )
} }
private fun createMoonPayModel(): TestModel {
return TestModel(
id = ApiConfig.ID.MoonPay,
expected = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.moonpay.com/",
),
)
}
private fun String.checkHeaderValueOrEmpty(): String { private fun String.checkHeaderValueOrEmpty(): String {
for (i in this.indices) { for (i in this.indices) {
val c = this[i] val c = this[i]

View file

@ -6,18 +6,21 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@ -34,6 +37,7 @@ data class SmallButtonConfig(
val onClick: () -> Unit, val onClick: () -> Unit,
val icon: TangemButtonIconPosition = TangemButtonIconPosition.None, val icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
val isEnabled: Boolean = true, val isEnabled: Boolean = true,
val isLoading: Boolean = false,
) )
/** /**
@ -68,7 +72,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
label = "Update background color", label = "Update background color",
) )
Row( Box(
modifier = modifier modifier = modifier
.defaultMinSize( .defaultMinSize(
minWidth = TangemTheme.dimens.size46, minWidth = TangemTheme.dimens.size46,
@ -79,7 +83,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
color = backgroundColor, color = backgroundColor,
shape = shape, shape = shape,
) )
.clickable(enabled = config.isEnabled, onClick = config.onClick) .clickable(enabled = !config.isLoading && config.isEnabled, onClick = config.onClick)
.padding( .padding(
paddingValues = when (config.icon) { paddingValues = when (config.icon) {
is TangemButtonIconPosition.None -> PaddingValues( is TangemButtonIconPosition.None -> PaddingValues(
@ -95,42 +99,54 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
) )
}, },
), ),
verticalAlignment = Alignment.CenterVertically, contentAlignment = Alignment.Center,
horizontalArrangement = Arrangement.Center,
) { ) {
ContentContainer( if (config.isLoading) {
iconPosition = config.icon, CircularProgressIndicator(
text = { strokeWidth = TangemTheme.dimens.size2,
val textColor by animateColorAsState( color = TangemTheme.colors.text.tertiary,
targetValue = when { modifier = Modifier.size(TangemTheme.dimens.size16),
!config.isEnabled -> TangemTheme.colors.text.disabled )
isPrimary -> TangemTheme.colors.text.primary2 }
else -> TangemTheme.colors.text.primary1 Row(
}, modifier = Modifier.conditional(config.isLoading) { alpha(0f) },
label = "Update text color", verticalAlignment = Alignment.CenterVertically,
) horizontalArrangement = Arrangement.Center,
) {
ContentContainer(
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = when {
!config.isEnabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
Text( Text(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4), modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
text = config.text.resolveReference(), text = config.text.resolveReference(),
color = textColor, color = textColor,
maxLines = 1, maxLines = 1,
style = TangemTheme.typography.button, style = TangemTheme.typography.button,
) )
}, },
icon = { iconResId -> icon = { iconResId ->
Icon( Icon(
modifier = Modifier.size(TangemTheme.dimens.size16), modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId), painter = painterResource(id = iconResId),
tint = if (config.isEnabled) { tint = if (config.isEnabled) {
TangemTheme.colors.icon.secondary TangemTheme.colors.icon.secondary
} else { } else {
TangemTheme.colors.icon.inactive TangemTheme.colors.icon.inactive
}, },
contentDescription = null, contentDescription = null,
) )
}, },
) )
}
} }
} }
@ -172,6 +188,7 @@ private fun ButtonsSample() {
) )
PrimarySmallButton(config = config) PrimarySmallButton(config = config)
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"))) SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add")))
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"), isLoading = true))
SecondarySmallButton( SecondarySmallButton(
config = config.copy( config = config.copy(
text = TextReference.Str(value = "Rating"), text = TextReference.Str(value = "Rating"),
@ -191,5 +208,12 @@ private fun ButtonsSample() {
isEnabled = false, isEnabled = false,
), ),
) )
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Add token"),
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
isLoading = true,
),
)
} }
} }

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -133,6 +135,13 @@ fun ActionBaseButton(
} }
} }
.clip(shape) .clip(shape)
.semantics {
contentDescription = if (config.shouldDimContent) {
"Action button is dimmed"
} else {
"Action button is not dimmed"
}
}
.combinedClickable( .combinedClickable(
enabled = config.isEnabled, enabled = config.isEnabled,
onClick = config.onClick, onClick = config.onClick,

View file

@ -0,0 +1,79 @@
package com.tangem.core.ui.components.feature
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewFeatureBlock() {
TangemThemePreview {
Column(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(16.dp),
) {
FeatureBlock(
title = stringResourceSafe(R.string.backup_info_save_title),
description = stringResourceSafe(R.string.backup_info_save_description, "12"),
iconRes = R.drawable.ic_lock_24,
)
Spacer(modifier = Modifier.height(24.dp))
FeatureBlock(
title = stringResourceSafe(R.string.backup_info_keep_title),
description = stringResourceSafe(R.string.backup_info_keep_description),
iconRes = R.drawable.ic_settings_24,
)
}
}
}

View file

@ -140,7 +140,9 @@ fun InputRowRecipient(
onClick = onPasteClick, onClick = onPasteClick,
backgroundColorEnabled = TangemTheme.colors.button.secondary, backgroundColorEnabled = TangemTheme.colors.button.secondary,
textColor = TangemTheme.colors.text.primary1, textColor = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.testTag(SendAddressScreenTestTags.ADDRESS_PASTE_BUTTON),
) )
} }
} }
@ -224,6 +226,7 @@ private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) {
text = state.address, text = state.address,
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SendAddressScreenTestTags.RESOLVED_ADDRESS),
) )
} }
} }

View file

@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
* @param onImageError composable to show if image loading failed * @param onImageError composable to show if image loading failed
*/ */
@Composable @Composable
internal fun InputRowAsyncImage( fun InputRowAsyncImage(
imageUrl: String, imageUrl: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isGrayscale: Boolean = false, isGrayscale: Boolean = false,

View file

@ -53,7 +53,7 @@ fun SelectorRowItem(
val textStyle = if (isSelected && showSelectedAppearance) { val textStyle = if (isSelected && showSelectedAppearance) {
TangemTheme.typography.subtitle2 TangemTheme.typography.subtitle2
} else { } else {
TangemTheme.typography.body2 TangemTheme.typography.body1
} }
Box( Box(
modifier = modifier modifier = modifier

View file

@ -27,6 +27,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -691,7 +692,10 @@ object AccountItemPreviewData {
value = stringReference("24 tokens"), value = stringReference("24 tokens"),
isAvailable = false, isAvailable = false,
), ),
subtitle2State = null, subtitle2State = Subtitle2State.PriceChangeContent(
priceChangePercent = "0,43 %",
type = PriceChangeType.UP,
),
onItemClick = {}, onItemClick = {},
onItemLongClick = {}, onItemLongClick = {},
) )

View file

@ -11,7 +11,6 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -31,7 +30,7 @@ internal fun TokenCryptoAmount(
isFlickering = state.isFlickering, isFlickering = state.isFlickering,
) )
} }
is TokenItemState.Subtitle2State.LabelContent -> { is TokenCryptoAmountState.LabelContent -> {
AuditLabel(state = state.auditLabelUM, modifier = modifier) AuditLabel(state = state.auditLabelUM, modifier = modifier)
} }
is TokenCryptoAmountState.Unreachable -> { is TokenCryptoAmountState.Unreachable -> {
@ -46,6 +45,15 @@ internal fun TokenCryptoAmount(
is TokenCryptoAmountState.Locked -> { is TokenCryptoAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize()) LockedRectangle(modifier = modifier.placeholderSize())
} }
is TokenCryptoAmountState.PriceChangeContent -> {
PriceBlock(
modifier = modifier,
price = null,
type = state.type,
priceChangePercent = state.priceChangePercent,
isFlickering = state.isFlickering,
)
}
null -> Unit null -> Unit
} }
} }

View file

@ -64,8 +64,8 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier)
} }
@Composable @Composable
private fun PriceBlock( internal fun PriceBlock(
price: String, price: String?,
isFlickering: Boolean, isFlickering: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
type: PriceChangeType? = null, type: PriceChangeType? = null,
@ -75,13 +75,15 @@ private fun PriceBlock(
modifier = modifier, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
PriceText( if (price != null) {
modifier = Modifier.weight(weight = 1f, fill = false), PriceText(
text = price, modifier = Modifier.weight(weight = 1f, fill = false),
isFlickering = isFlickering, text = price,
) isFlickering = isFlickering,
)
SpacerW6() SpacerW6()
}
if (type != null) { if (type != null) {
PriceChangeIcon( PriceChangeIcon(

View file

@ -230,6 +230,12 @@ sealed class TokenItemState {
val isFlickering: Boolean = false, val isFlickering: Boolean = false,
) : Subtitle2State() ) : Subtitle2State()
data class PriceChangeContent(
val priceChangePercent: String,
val type: PriceChangeType,
val isFlickering: Boolean = false,
) : Subtitle2State()
data class LabelContent(val auditLabelUM: AuditLabelUM) : Subtitle2State() data class LabelContent(val auditLabelUM: AuditLabelUM) : Subtitle2State()
data object Unreachable : Subtitle2State() data object Unreachable : Subtitle2State()

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
/**
* Utility class for keeping themed color reference from app theme.
*
* It necessary to use [Immutable] annotation for runtime stability.
*
* @property value color provider from theme
*/
@Immutable
data class ColorReference(val value: @Composable () -> Color)
/**
* Creates a [ColorReference] using a themed color from the app theme with a lambda.
*
* @param value The color provider from theme.
* @return A [ColorReference] representing the themed color.
*/
fun themedColor(value: @Composable () -> Color): ColorReference {
return ColorReference(value)
}
/**
* Resolves [ColorReference] to [Color]
*/
@Composable
fun ColorReference.resolveReference(): Color {
return value()
}

View file

@ -20,6 +20,7 @@ object TangemColorPalette {
// region Light // region Light
val Light1 = Color(0xFFF5F5F5) val Light1 = Color(0xFFF5F5F5)
val Light1V2 = Color(0xFFF4F4F4)
val Light2 = Color(0xFFEBEBEB) val Light2 = Color(0xFFEBEBEB)
val Light3 = Color(0xFFD3D3D3) val Light3 = Color(0xFFD3D3D3)
val Light4 = Color(0xFFC9C9C9) val Light4 = Color(0xFFC9C9C9)
@ -27,6 +28,7 @@ object TangemColorPalette {
// endregion Light // endregion Light
// region Green // region Green
val Green = Color(0xFF0C9F3D)
val Meadow = Color(0xFF1ACE80) val Meadow = Color(0xFF1ACE80)
val MagicMint = Color(0xFFA3EBCC) val MagicMint = Color(0xFFA3EBCC)
val DarkGreen = Color(0xFF06311F) val DarkGreen = Color(0xFF06311F)
@ -45,4 +47,9 @@ object TangemColorPalette {
val Tangerine = Color(0xFFFFB71B) val Tangerine = Color(0xFFFFB71B)
val Mustard = Color(0xFFFDDE55) val Mustard = Color(0xFFFDDE55)
// endregion Yellow // endregion Yellow
// region Overlay
val Overlay1 = Color(0x66000000)
val Overlay2 = Color(0xB2000000)
// endregion Overlay
} }

View file

@ -0,0 +1,536 @@
@file:Suppress("LongParameterList")
package com.tangem.core.ui.res
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
@Stable
class TangemColors2 internal constructor(
val text: Text,
val graphic: Graphic,
val button: Button,
val surface: Surface,
val controls: Controls,
val field: Field,
val overlay: Overlay,
val border: Border,
val fill: Fill,
val skeleton: Skeleton,
val markers: Markers,
) {
@Stable
class Text internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
primaryInverted: Color,
secondary: Color,
tertiary: Color,
primaryInvertedConstant: Color,
) {
var primary by mutableStateOf(primary)
private set
var primaryInverted by mutableStateOf(primaryInverted)
private set
var secondary by mutableStateOf(secondary)
private set
var tertiary by mutableStateOf(tertiary)
private set
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
private set
fun update(other: Neutral) {
primary = other.primary
primaryInverted = other.primaryInverted
secondary = other.secondary
tertiary = other.tertiary
primaryInvertedConstant = other.primaryInvertedConstant
}
}
@Stable
class Status internal constructor(
disabled: Color,
accent: Color,
warning: Color,
attention: Color,
positive: Color,
) {
var disabled by mutableStateOf(disabled)
private set
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
var positive by mutableStateOf(positive)
private set
fun update(other: Status) {
disabled = other.disabled
accent = other.accent
warning = other.warning
attention = other.attention
positive = other.positive
}
}
fun update(other: Text) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Graphic internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
primaryInverted: Color,
secondary: Color,
tertiary: Color,
quaternary: Color,
primaryInvertedConstant: Color,
tertiaryConstant: Color,
) {
var primary by mutableStateOf(primary)
private set
var primaryInverted by mutableStateOf(primaryInverted)
private set
var secondary by mutableStateOf(secondary)
private set
var tertiary by mutableStateOf(tertiary)
private set
var quaternary by mutableStateOf(quaternary)
private set
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
private set
var tertiaryConstant by mutableStateOf(tertiaryConstant)
private set
fun update(other: Neutral) {
primary = other.primary
primaryInverted = other.primaryInverted
secondary = other.secondary
tertiary = other.tertiary
quaternary = other.quaternary
primaryInvertedConstant = other.primaryInvertedConstant
tertiaryConstant = other.tertiaryConstant
}
}
@Stable
class Status internal constructor(
accent: Color,
warning: Color,
attention: Color,
) {
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
fun update(other: Status) {
accent = other.accent
warning = other.warning
attention = other.attention
}
}
fun update(other: Graphic) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Button internal constructor(
backgroundPrimary: Color,
backgroundSecondary: Color,
backgroundDisabled: Color,
backgroundPositive: Color,
textPrimary: Color,
textSecondary: Color,
textDisabled: Color,
iconPrimary: Color,
iconSecondary: Color,
iconDisabled: Color,
borderPrimary: Color,
) {
var backgroundPrimary by mutableStateOf(backgroundPrimary)
private set
var backgroundSecondary by mutableStateOf(backgroundSecondary)
private set
var backgroundDisabled by mutableStateOf(backgroundDisabled)
private set
var backgroundPositive by mutableStateOf(backgroundPositive)
private set
var textPrimary by mutableStateOf(textPrimary)
private set
var textSecondary by mutableStateOf(textSecondary)
private set
var textDisabled by mutableStateOf(textDisabled)
private set
var iconPrimary by mutableStateOf(iconPrimary)
private set
var iconSecondary by mutableStateOf(iconSecondary)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
var borderPrimary by mutableStateOf(borderPrimary)
private set
fun update(other: Button) {
backgroundPrimary = other.backgroundPrimary
backgroundSecondary = other.backgroundSecondary
backgroundDisabled = other.backgroundDisabled
backgroundPositive = other.backgroundPositive
textPrimary = other.textPrimary
textSecondary = other.textSecondary
textDisabled = other.textDisabled
iconPrimary = other.iconPrimary
iconSecondary = other.iconSecondary
iconDisabled = other.iconDisabled
borderPrimary = other.borderPrimary
}
}
@Stable
class Surface internal constructor(
level1: Color,
level2: Color,
level3: Color,
level4: Color,
) {
var level1 by mutableStateOf(level1)
private set
var level2 by mutableStateOf(level2)
private set
var level3 by mutableStateOf(level3)
private set
var level4 by mutableStateOf(level4)
private set
fun update(other: Surface) {
level1 = other.level1
level2 = other.level2
level3 = other.level3
level4 = other.level4
}
}
@Stable
class Controls internal constructor(
backgroundDefault: Color,
backgroundChecked: Color,
iconDefault: Color,
iconDisabled: Color,
) {
var backgroundDefault by mutableStateOf(backgroundDefault)
private set
var backgroundChecked by mutableStateOf(backgroundChecked)
private set
var iconDefault by mutableStateOf(iconDefault)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
fun update(other: Controls) {
backgroundDefault = other.backgroundDefault
backgroundChecked = other.backgroundChecked
iconDefault = other.iconDefault
iconDisabled = other.iconDisabled
}
}
@Stable
class Field internal constructor(
backgroundDefault: Color,
backgroundFocused: Color,
textPlaceholder: Color,
textDefault: Color,
textDisabled: Color,
iconDefault: Color,
iconDisabled: Color,
textInvalid: Color,
borderInvalid: Color,
) {
var backgroundDefault by mutableStateOf(backgroundDefault)
private set
var backgroundFocused by mutableStateOf(backgroundFocused)
private set
var textPlaceholder by mutableStateOf(textPlaceholder)
private set
var textDefault by mutableStateOf(textDefault)
private set
var textDisabled by mutableStateOf(textDisabled)
private set
var iconDefault by mutableStateOf(iconDefault)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
var textInvalid by mutableStateOf(textInvalid)
private set
var borderInvalid by mutableStateOf(borderInvalid)
private set
fun update(other: Field) {
backgroundDefault = other.backgroundDefault
backgroundFocused = other.backgroundFocused
textPlaceholder = other.textPlaceholder
textDefault = other.textDefault
textDisabled = other.textDisabled
iconDefault = other.iconDefault
iconDisabled = other.iconDisabled
textInvalid = other.textInvalid
borderInvalid = other.borderInvalid
}
}
@Stable
class Overlay internal constructor(
overlayPrimary: Color,
overlaySecondary: Color,
) {
var overlayPrimary by mutableStateOf(overlayPrimary)
private set
var overlaySecondary by mutableStateOf(overlaySecondary)
private set
fun update(other: Overlay) {
overlayPrimary = other.overlayPrimary
overlaySecondary = other.overlaySecondary
}
}
@Stable
class Border internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
secondary: Color,
) {
var primary by mutableStateOf(primary)
private set
var secondary by mutableStateOf(secondary)
private set
fun update(other: Neutral) {
primary = other.primary
secondary = other.secondary
}
}
@Stable
class Status internal constructor(
accent: Color,
warning: Color,
attention: Color,
) {
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
fun update(other: Status) {
accent = other.accent
warning = other.warning
attention = other.attention
}
}
fun update(other: Border) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Fill internal constructor(
val neutral: Neutral,
val status: Status,
) {
@Stable
class Neutral internal constructor(
primary: Color,
primaryInverted: Color,
primaryInvertedConstant: Color,
secondary: Color,
tertiaryConstant: Color,
quaternary: Color,
) {
var primary by mutableStateOf(primary)
private set
var primaryInverted by mutableStateOf(primaryInverted)
private set
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
private set
var secondary by mutableStateOf(secondary)
private set
var tertiaryConstant by mutableStateOf(tertiaryConstant)
private set
var quaternary by mutableStateOf(quaternary)
private set
fun update(other: Neutral) {
primary = other.primary
primaryInverted = other.primaryInverted
primaryInvertedConstant = other.primaryInvertedConstant
secondary = other.secondary
tertiaryConstant = other.tertiaryConstant
quaternary = other.quaternary
}
}
@Stable
class Status internal constructor(
accent: Color,
warning: Color,
attention: Color,
) {
var accent by mutableStateOf(accent)
private set
var warning by mutableStateOf(warning)
private set
var attention by mutableStateOf(attention)
private set
fun update(other: Status) {
accent = other.accent
warning = other.warning
attention = other.attention
}
}
fun update(other: Fill) {
neutral.update(other.neutral)
status.update(other.status)
}
}
@Stable
class Skeleton internal constructor(
backgroundPrimary: Color,
) {
var backgroundPrimary by mutableStateOf(backgroundPrimary)
private set
fun update(other: Skeleton) {
backgroundPrimary = other.backgroundPrimary
}
}
@Stable
class Markers internal constructor(
backgroundSolidGray: Color,
backgroundDisabled: Color,
backgroundSolidBlue: Color,
textGray: Color,
textDisabled: Color,
iconGray: Color,
iconDisabled: Color,
borderGray: Color,
backgroundTintedBlue: Color,
textBlue: Color,
backgroundSolidRed: Color,
backgroundTintedRed: Color,
iconBlue: Color,
iconRed: Color,
textRed: Color,
backgroundTintedGray: Color,
borderTintedBlue: Color,
borderTintedRed: Color,
) {
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
private set
var backgroundDisabled by mutableStateOf(backgroundDisabled)
private set
var backgroundSolidBlue by mutableStateOf(backgroundSolidBlue)
private set
var textGray by mutableStateOf(textGray)
private set
var textDisabled by mutableStateOf(textDisabled)
private set
var iconGray by mutableStateOf(iconGray)
private set
var iconDisabled by mutableStateOf(iconDisabled)
private set
var borderGray by mutableStateOf(borderGray)
private set
var backgroundTintedBlue by mutableStateOf(backgroundTintedBlue)
private set
var textBlue by mutableStateOf(textBlue)
private set
var backgroundSolidRed by mutableStateOf(backgroundSolidRed)
private set
var backgroundTintedRed by mutableStateOf(backgroundTintedRed)
private set
var iconBlue by mutableStateOf(iconBlue)
private set
var iconRed by mutableStateOf(iconRed)
private set
var textRed by mutableStateOf(textRed)
private set
var backgroundTintedGray by mutableStateOf(backgroundTintedGray)
private set
var borderTintedBlue by mutableStateOf(borderTintedBlue)
private set
var borderTintedRed by mutableStateOf(borderTintedRed)
private set
fun update(other: Markers) {
backgroundSolidGray = other.backgroundSolidGray
backgroundDisabled = other.backgroundDisabled
backgroundSolidBlue = other.backgroundSolidBlue
textGray = other.textGray
textDisabled = other.textDisabled
iconGray = other.iconGray
iconDisabled = other.iconDisabled
borderGray = other.borderGray
backgroundTintedBlue = other.backgroundTintedBlue
textBlue = other.textBlue
backgroundSolidRed = other.backgroundSolidRed
backgroundTintedRed = other.backgroundTintedRed
iconBlue = other.iconBlue
iconRed = other.iconRed
textRed = other.textRed
backgroundTintedGray = other.backgroundTintedGray
borderTintedBlue = other.borderTintedBlue
borderTintedRed = other.borderTintedRed
}
}
fun update(other: TangemColors2) {
text.update(other.text)
graphic.update(other.graphic)
button.update(other.button)
surface.update(other.surface)
controls.update(other.controls)
field.update(other.field)
overlay.update(other.overlay)
border.update(other.border)
fill.update(other.fill)
skeleton.update(other.skeleton)
markers.update(other.markers)
}
}

View file

@ -138,6 +138,11 @@ object TangemTheme {
@ReadOnlyComposable @ReadOnlyComposable
get() = LocalTangemColors.current get() = LocalTangemColors.current
val colors2: TangemColors2
@Composable
@ReadOnlyComposable
get() = LocalTangemColors2.current
val typography: TangemTypography val typography: TangemTypography
@Composable @Composable
@ReadOnlyComposable @ReadOnlyComposable
@ -156,7 +161,7 @@ object TangemTheme {
@Stable @Stable
@Composable @Composable
private fun tangemColorScheme(colors: TangemColors): ColorScheme { internal fun tangemColorScheme(colors: TangemColors): ColorScheme {
return ColorScheme( return ColorScheme(
primary = colors.background.primary, primary = colors.background.primary,
onPrimary = colors.text.primary1, onPrimary = colors.text.primary1,
@ -206,7 +211,7 @@ private fun tangemColorScheme(colors: TangemColors): ColorScheme {
@Composable @Composable
@ReadOnlyComposable @ReadOnlyComposable
private fun lightThemeColors(): TangemColors { internal fun lightThemeColors(redesign: Boolean = false): TangemColors {
return TangemColors( return TangemColors(
text = TangemColors.Text( text = TangemColors.Text(
primary1 = TangemColorPalette.Dark6, primary1 = TangemColorPalette.Dark6,
@ -233,8 +238,8 @@ private fun lightThemeColors(): TangemColors {
), ),
background = TangemColors.Background( background = TangemColors.Background(
primary = TangemColorPalette.White, primary = TangemColorPalette.White,
secondary = TangemColorPalette.Light1, secondary = if (redesign) TangemColorPalette.Light1V2 else TangemColorPalette.Light1,
tertiary = TangemColorPalette.Light1, tertiary = if (redesign) TangemColorPalette.Light1V2 else TangemColorPalette.Light1,
action = TangemColorPalette.White, action = TangemColorPalette.White,
), ),
control = TangemColors.Control( control = TangemColors.Control(
@ -248,7 +253,7 @@ private fun lightThemeColors(): TangemColors {
transparency = TangemColorPalette.White, transparency = TangemColorPalette.White,
), ),
field = TangemColors.Field( field = TangemColors.Field(
primary = TangemColorPalette.Light1, primary = if (redesign) TangemColorPalette.Light1V2 else TangemColorPalette.Light1,
focused = TangemColorPalette.Light2, focused = TangemColorPalette.Light2,
), ),
overlay = TangemColors.Overlay( overlay = TangemColors.Overlay(
@ -260,7 +265,7 @@ private fun lightThemeColors(): TangemColors {
@Composable @Composable
@ReadOnlyComposable @ReadOnlyComposable
private fun darkThemeColors(): TangemColors { internal fun darkThemeColors(): TangemColors {
return TangemColors( return TangemColors(
text = TangemColors.Text( text = TangemColors.Text(
primary1 = TangemColorPalette.White, primary1 = TangemColorPalette.White,
@ -320,12 +325,16 @@ private val TangemTextSelectionColors: TextSelectionColors
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f), backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
) )
private val LocalTangemColors = staticCompositionLocalOf<TangemColors> { internal val LocalTangemColors = staticCompositionLocalOf<TangemColors> {
error("No TangemColors provided") error("No TangemColors provided")
} }
private val LocalTangemTypography = staticCompositionLocalOf { internal val LocalTangemColors2 = staticCompositionLocalOf<TangemColors2> {
TangemTypography() error("No TangemColors2 provided")
}
internal val LocalTangemTypography = staticCompositionLocalOf {
TangemTypography(RobotoFamily)
} }
private val LocalTangemDimens = staticCompositionLocalOf { private val LocalTangemDimens = staticCompositionLocalOf {

View file

@ -0,0 +1,309 @@
@file:Suppress("LongMethod")
package com.tangem.core.ui.res
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
/**
* Provides additional theming for redesigned components.
* Used together with [TangemTheme].
* @param content Composable content where the theme is applied.
*/
@Composable
fun TangemThemeRedesign(content: @Composable () -> Unit) {
val themeColors = if (LocalIsInDarkTheme.current) darkThemeColors() else lightThemeColors(redesign = true)
val rememberedColors = remember { themeColors }
.also { it.update(themeColors) }
val rootBackgroundColor = rememberedColors.background.secondary
MaterialTheme(
colorScheme = tangemColorScheme(colors = themeColors),
) {
CompositionLocalProvider(
LocalTangemColors provides themeColors,
LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(),
LocalTangemTypography provides TangemTypography(InterFamily),
LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) },
) {
content()
}
}
}
@Composable
@ReadOnlyComposable
private fun lightThemeColors2(): TangemColors2 {
val text = TangemColors2.Text(
neutral = TangemColors2.Text.Neutral(
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
secondary = TangemColorPalette.Dark2,
tertiary = TangemColorPalette.Dark3,
primaryInvertedConstant = TangemColorPalette.White,
),
status = TangemColors2.Text.Status(
disabled = TangemColorPalette.Light4,
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
positive = TangemColorPalette.Green,
),
)
val graphic = TangemColors2.Graphic(
neutral = TangemColors2.Graphic.Neutral(
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
secondary = TangemColorPalette.Dark2,
tertiary = TangemColorPalette.Dark3,
quaternary = TangemColorPalette.Light4,
primaryInvertedConstant = TangemColorPalette.White,
tertiaryConstant = TangemColorPalette.Dark3,
),
status = TangemColors2.Graphic.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
)
val border = TangemColors2.Border(
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Light3,
secondary = TangemColorPalette.Light5,
),
status = TangemColors2.Border.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
)
val overlay = TangemColors2.Overlay(
overlayPrimary = TangemColorPalette.Overlay1,
overlaySecondary = TangemColorPalette.Overlay2,
)
val fill = TangemColors2.Fill(
neutral = TangemColors2.Fill.Neutral(
primary = TangemColorPalette.Dark6,
primaryInverted = TangemColorPalette.White,
primaryInvertedConstant = TangemColorPalette.White,
secondary = TangemColorPalette.Dark3,
tertiaryConstant = TangemColorPalette.Dark3,
quaternary = TangemColorPalette.Light4,
),
status = TangemColors2.Fill.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Amaranth,
attention = TangemColorPalette.Tangerine,
),
)
val button = TangemColors2.Button(
backgroundPrimary = TangemColorPalette.Dark6,
backgroundSecondary = TangemColorPalette.Dark6.copy(alpha = 0.1f),
backgroundDisabled = TangemColorPalette.Light3,
backgroundPositive = TangemColorPalette.Azure,
textSecondary = TangemColorPalette.Dark6,
textPrimary = TangemColorPalette.Light2,
textDisabled = text.neutral.tertiary,
iconPrimary = TangemColorPalette.Dark6,
iconSecondary = TangemColorPalette.Light1V2,
iconDisabled = TangemColorPalette.Light2,
borderPrimary = TangemColorPalette.Dark6,
)
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.White,
level2 = TangemColorPalette.Light1V2,
level3 = TangemColorPalette.Light1V2,
level4 = TangemColorPalette.White,
)
val controls = TangemColors2.Controls(
backgroundChecked = TangemColorPalette.Dark6,
backgroundDefault = TangemColorPalette.Light2,
iconDefault = TangemColorPalette.White,
iconDisabled = TangemColorPalette.White,
)
val field = TangemColors2.Field(
backgroundDefault = TangemColorPalette.Light1V2,
backgroundFocused = TangemColorPalette.Light3,
textPlaceholder = text.neutral.secondary,
textDefault = text.neutral.primary,
textDisabled = text.neutral.tertiary,
iconDefault = graphic.neutral.tertiary,
iconDisabled = graphic.neutral.quaternary,
textInvalid = text.status.warning,
borderInvalid = border.status.warning,
)
val skeleton = TangemColors2.Skeleton(
backgroundPrimary = TangemColorPalette.Light1V2,
)
val markers = TangemColors2.Markers(
backgroundSolidGray = TangemColorPalette.Light3,
backgroundDisabled = TangemColorPalette.Light3,
backgroundSolidBlue = TangemColorPalette.Azure,
textGray = TangemColorPalette.Dark2,
textDisabled = text.neutral.tertiary,
iconGray = TangemColorPalette.Dark1,
iconDisabled = TangemColorPalette.Light2,
borderGray = TangemColorPalette.Light3,
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
iconBlue = TangemColorPalette.Azure,
iconRed = TangemColorPalette.Amaranth,
textRed = TangemColorPalette.Amaranth,
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
return TangemColors2(
text = text,
graphic = graphic,
border = border,
overlay = overlay,
fill = fill,
button = button,
surface = surface,
controls = controls,
field = field,
skeleton = skeleton,
markers = markers,
)
}
@Composable
@ReadOnlyComposable
private fun darkThemeColors2(): TangemColors2 {
val text = TangemColors2.Text(
neutral = TangemColors2.Text.Neutral(
primary = TangemColorPalette.White,
primaryInverted = TangemColorPalette.Dark6,
secondary = TangemColorPalette.Light5,
tertiary = TangemColorPalette.Dark1,
primaryInvertedConstant = TangemColorPalette.White,
),
status = TangemColors2.Text.Status(
disabled = TangemColorPalette.Dark3,
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
positive = TangemColorPalette.Green,
),
)
val graphic = TangemColors2.Graphic(
neutral = TangemColors2.Graphic.Neutral(
primary = TangemColorPalette.White,
primaryInverted = TangemColorPalette.Dark6,
secondary = TangemColorPalette.Light5,
tertiary = TangemColorPalette.Dark3,
quaternary = TangemColorPalette.Dark3,
tertiaryConstant = TangemColorPalette.Dark3,
primaryInvertedConstant = TangemColorPalette.White,
),
status = TangemColors2.Graphic.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
)
val border = TangemColors2.Border(
neutral = TangemColors2.Border.Neutral(
primary = TangemColorPalette.Dark4,
secondary = TangemColorPalette.Dark4,
),
status = TangemColors2.Border.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
)
val overlay = TangemColors2.Overlay(
overlayPrimary = TangemColorPalette.Overlay1,
overlaySecondary = TangemColorPalette.Overlay2,
)
val fill = TangemColors2.Fill(
neutral = TangemColors2.Fill.Neutral(
primary = TangemColorPalette.White,
primaryInverted = TangemColorPalette.Dark6,
primaryInvertedConstant = TangemColorPalette.White,
secondary = TangemColorPalette.Light5,
tertiaryConstant = TangemColorPalette.Dark1,
quaternary = TangemColorPalette.Dark3,
),
status = TangemColors2.Fill.Status(
accent = TangemColorPalette.Azure,
warning = TangemColorPalette.Flamingo,
attention = TangemColorPalette.Mustard,
),
)
val button = TangemColors2.Button(
backgroundPrimary = TangemColorPalette.Light1V2,
backgroundSecondary = TangemColorPalette.White.copy(alpha = 0.1f),
backgroundDisabled = TangemColorPalette.Dark5,
backgroundPositive = TangemColorPalette.Azure,
textSecondary = TangemColorPalette.Light4,
textPrimary = TangemColorPalette.Dark4,
textDisabled = text.neutral.secondary,
iconPrimary = TangemColorPalette.Light4,
iconSecondary = TangemColorPalette.Dark4,
iconDisabled = TangemColorPalette.Dark5,
borderPrimary = TangemColorPalette.Light4,
)
val surface = TangemColors2.Surface(
level1 = TangemColorPalette.Dark6,
level2 = TangemColorPalette.Black,
level3 = TangemColorPalette.Dark6,
level4 = TangemColorPalette.Dark5,
)
val controls = TangemColors2.Controls(
backgroundChecked = TangemColorPalette.Azure,
backgroundDefault = TangemColorPalette.Dark4,
iconDefault = TangemColorPalette.White,
iconDisabled = TangemColorPalette.White,
)
val field = TangemColors2.Field(
backgroundDefault = TangemColorPalette.Dark6,
backgroundFocused = TangemColorPalette.Dark4,
textPlaceholder = text.neutral.secondary,
textDefault = text.neutral.primary,
textDisabled = text.neutral.tertiary,
iconDefault = graphic.neutral.tertiary,
iconDisabled = graphic.neutral.quaternary,
textInvalid = text.status.warning,
borderInvalid = border.status.warning,
)
val skeleton = TangemColors2.Skeleton(
backgroundPrimary = TangemColorPalette.Dark5,
)
val markers = TangemColors2.Markers(
backgroundSolidGray = TangemColorPalette.Dark5,
backgroundDisabled = TangemColorPalette.Dark5,
backgroundSolidBlue = TangemColorPalette.Azure,
textGray = TangemColorPalette.Light4,
textDisabled = text.neutral.secondary,
iconGray = TangemColorPalette.Dark2,
iconDisabled = TangemColorPalette.Dark5,
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
iconBlue = TangemColorPalette.Azure,
iconRed = TangemColorPalette.Flamingo,
textRed = TangemColorPalette.Flamingo,
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
return TangemColors2(
text = text,
graphic = graphic,
border = border,
overlay = overlay,
fill = fill,
button = button,
surface = surface,
controls = controls,
field = field,
skeleton = skeleton,
markers = markers,
)
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
@ -11,15 +12,22 @@ import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.tangem.core.ui.R import com.tangem.core.ui.R
private val RobotoFamily = FontFamily( internal val RobotoFamily = FontFamily(
Font(R.font.roboto_regular, FontWeight.Normal), Font(R.font.roboto_regular, FontWeight.Normal),
Font(R.font.roboto_medium, FontWeight.Medium), Font(R.font.roboto_medium, FontWeight.Medium),
) )
internal val InterFamily = FontFamily(
Font(R.font.inter_regular),
Font(R.font.inter_italic, style = FontStyle.Italic),
)
@Immutable @Immutable
data class TangemTypography internal constructor( class TangemTypography internal constructor(
fontFamily: FontFamily,
) {
val head: TextStyle = TextStyle( val head: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 34.sp, fontSize = 34.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
@ -28,9 +36,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val h1: TextStyle = TextStyle( val h1: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 34.sp, fontSize = 34.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
@ -39,9 +47,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val h2: TextStyle = TextStyle( val h2: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 24.sp, fontSize = 24.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.18f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.18f, type = TextUnitType.Sp),
@ -50,9 +58,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val h3: TextStyle = TextStyle( val h3: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp),
@ -61,9 +69,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val subtitle1: TextStyle = TextStyle( val subtitle1: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp),
@ -72,9 +80,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val subtitle2: TextStyle = TextStyle( val subtitle2: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
@ -83,9 +91,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val body1: TextStyle = TextStyle( val body1: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp),
@ -94,9 +102,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val body2: TextStyle = TextStyle( val body2: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.25f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.25f, type = TextUnitType.Sp),
@ -105,9 +113,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val button: TextStyle = TextStyle( val button: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
@ -116,9 +124,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val caption1: TextStyle = TextStyle( val caption1: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp),
@ -127,9 +135,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val caption2: TextStyle = TextStyle( val caption2: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp),
@ -138,9 +146,9 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
val overline: TextStyle = TextStyle( val overline: TextStyle = TextStyle(
fontFamily = RobotoFamily, fontFamily = fontFamily,
fontSize = 10.sp, fontSize = 10.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 1.5f, type = TextUnitType.Sp), letterSpacing = TextUnit(value = 1.5f, type = TextUnitType.Sp),
@ -149,5 +157,5 @@ data class TangemTypography internal constructor(
alignment = LineHeightStyle.Alignment.Center, alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None, trim = LineHeightStyle.Trim.None,
), ),
), )
) }

View file

@ -1,6 +1,6 @@
package com.tangem.core.ui.test package com.tangem.core.ui.test
object BaseAmountBlockTestTags { object BaseAmountBlockTestTags {
const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" const val PRIMARY_AMOUNT = "SEND_DETAILS_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" const val SECONDARY_AMOUNT = "SEND_DETAILS_SCREEN_SECONDARY_AMOUNT"
} }

View file

@ -1,6 +1,17 @@
package com.tangem.core.ui.test package com.tangem.core.ui.test
object SendAddressScreenTestTags { object SendAddressScreenTestTags {
const val ADDRESS_TEXT_FIELD_TITLE = "SEND_ADDRESS_TEXT_FIELD_TITLE" const val ADDRESS_TEXT_FIELD_TITLE = "SEND_ADDRESS_SCREEN_TEXT_FIELD_TITLE"
const val ADDRESS_TEXT_FIELD = "SEND_ADDRESS_TEXT_FIELD" const val ADDRESS_TEXT_FIELD = "SEND_ADDRESS_SCREEN_TEXT_FIELD"
const val ADDRESS_PASTE_BUTTON = "SEND_ADDRESS_SCREEN_ADDRESS_PASTE_BUTTON"
const val RESOLVED_ADDRESS = "SEND_ADDRESS_SCREEN_RESOLVED_ADDRESS"
const val RECENT_ADDRESS_TITLE = "SEND_ADDRESS_SCREEN_RECENT_ADDRESS_TITLE"
const val RECENT_ADDRESS_TEXT = "SEND_ADDRESS_SCREEN_RECENT_ADDRESS_TEXT"
const val RECENT_ADDRESS_ICON = "SEND_ADDRESS_SCREEN_RECENT_ADDRESS_ICON"
const val DESTINATION_TAG_TEXT_FIELD_TITLE = "SEND_ADDRESS_SCREEN_DESTINATION_TAG_TEXT_FIELD_TITLE"
const val DESTINATION_TAG_TEXT_FIELD = "SEND_ADDRESS_SCREEN_DESTINATION_TAG_TEXT_FIELD"
const val DESTINATION_TAG_PASTE_BUTTON = "SEND_ADDRESS_SCREEN_DESTINATION_TAG_PASTE_BUTTON"
const val DESTINATION_TAG_CLEAR_TEXT_FIELD_BUTTON = "SEND_ADDRESS_SCREEN_DESTINATION_TAG_CLEAR_TEXT_FIELD_BUTTON"
} }

View file

@ -2,4 +2,7 @@ package com.tangem.core.ui.test
object SendConfirmScreenTestTags { object SendConfirmScreenTestTags {
const val SENDING_TEXT = "SEND_CONFIRM_SCREEN_SENDING_TEXT" const val SENDING_TEXT = "SEND_CONFIRM_SCREEN_SENDING_TEXT"
const val RECIPIENT_ADDRESS = "SEND_CONFIRM_SCREEN_RECIPIENT_ADDRESS"
const val BLOCKCHAIN_ADDRESS = "SEND_CONFIRM_SCREEN_BLOCKCHAIN_ADDRESS"
const val RECIPIENT_ADDRESS_ICON = "SEND_CONFIRM_SCREEN_RECIPIENT_ADDRESS_ICON"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

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