Updated on 2026-08-14
This commit is contained in:
commit
31be70ecf3
529 changed files with 15730 additions and 4170 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) =
|
||||||
|
|
|
||||||
|
|
@ -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) =
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -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(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 -> {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -30,9 +30,5 @@
|
||||||
{
|
{
|
||||||
"name": "zklink",
|
"name": "zklink",
|
||||||
"version": "undefined"
|
"version": "undefined"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "scroll",
|
|
||||||
"version": "undefined"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package com.tangem.datasource.api.express.models
|
|
||||||
|
|
||||||
object TangemExpressValues {
|
|
||||||
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>>>
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
536
core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
Normal file
536
core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue