Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-04 19:37:14 +04:00
commit 304bf0df87
928 changed files with 23076 additions and 29831 deletions

View file

@ -73,15 +73,6 @@ android {
}
}
buildTypes {
debug {
buildConfigField("String", "BUILD_TYPE", "\"debug\"")
}
release {
buildConfigField("String", "BUILD_TYPE", "\"release\"")
}
}
}
configurations.all {
@ -152,6 +143,7 @@ dependencies {
implementation(projects.domain.manageTokens)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)
implementation(projects.domain.offramp)
implementation(projects.domain.onramp)
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
@ -304,6 +296,8 @@ dependencies {
implementation(projects.features.tokenRecieve.impl)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.yieldSupply.impl)
implementation(projects.features.approval.api)
implementation(projects.features.approval.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@ -380,6 +374,8 @@ dependencies {
implementation(deps.amplitude)
implementation(deps.appsflyer)
implementation(deps.appsflyer.oaid)
implementation(deps.customerio.analytics)
implementation(deps.customerio.messaging)
implementation("com.android.installreferrer:installreferrer:2.2")
implementation(deps.spongecastle.core)
implementation(deps.lottie)

View file

@ -146,12 +146,7 @@ abstract class BaseTestCase : TestCase(
return ApplicationInjectionExecutionRule(
toggleStates = mapOf(
"SWAP_REDESIGN_ENABLED" to false,
"NEW_ONRAMP_MAIN_ENABLED" to true,
"HOT_WALLET_ENABLED" to true,
"YIELD_SUPPLY_FEATURE_ENABLED" to true,
"ACCOUNTS_FEATURE_ENABLED" to true,
"FEED_ENABLED" to true,
"GASLESS_TRANSACTIONS_ENABLED" to true,
)
)
}

View file

@ -5,7 +5,6 @@ import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.*
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -14,7 +13,6 @@ import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.scanCard(
productType: ProductType? = null,
mockContent: MockContent? = null,
alreadyActivatedDialogIsShown: Boolean = false,
isTwinsCard: Boolean = false,
) {
if (productType != null) {
@ -32,12 +30,6 @@ fun BaseTestCase.scanCard(
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
}
if (alreadyActivatedDialogIsShown) {
step("Click on 'This is my wallet' button") {
waitForIdle()
AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() }
}
}
if (isTwinsCard) {
step("Click on 'Continue' button") {
onOnboardingScreen { continueButton.clickWithAssertion() }
@ -51,14 +43,12 @@ fun BaseTestCase.scanCard(
fun BaseTestCase.openMainScreen(
productType: ProductType? = null,
mockContent: MockContent? = null,
alreadyActivatedDialogIsShown: Boolean = false,
isTwinsCard: Boolean = false,
) {
step("Scan card") {
scanCard(
productType = productType,
mockContent = mockContent,
alreadyActivatedDialogIsShown = alreadyActivatedDialogIsShown,
isTwinsCard = isTwinsCard,
)
}

View file

@ -170,6 +170,45 @@ fun BaseTestCase.checkStoriesChanges() {
}
}
fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) {
step("Click on 'Select fee' icon") {
onSwapTokenScreen { selectFeeIcon.performClick() }
}
when (feeType) {
FeeType.Market -> {
step("Click on 'Market' item") {
onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.performClick() }
}
step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") {
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
}
}
FeeType.Fast -> {
step("Click on 'Fast' item") {
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.performClick() }
}
step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") {
onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) }
}
}
}
}
fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) {
step("Assert 'Unable to cover '$networkName' fee notification title is displayed'") {
onSwapTokenScreen { unableToCoverFeeNotificationTitle(networkName).assertIsDisplayed() }
}
step("Assert 'Unable to cover '$networkName' fee notification text is displayed'") {
onSwapTokenScreen {
unableToCoverFeeNotificationText(
currencyName = networkName,
currencySymbol = currencySymbol
).assertIsDisplayed()
}
}
}
sealed class SwapEntryPoint {
object MainScreen : SwapEntryPoint()
object TokenDetails : SwapEntryPoint()
@ -177,4 +216,9 @@ sealed class SwapEntryPoint {
object TokenActionsBottomSheet : SwapEntryPoint()
}
enum class FeeType {
Market,
Fast
}

View file

@ -3,7 +3,8 @@ 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.TokenElementsTestTags
import com.tangem.core.ui.test.AppBarWithSearchTestTags
import com.tangem.core.ui.test.BuyTokenScreenTestTags
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
@ -21,11 +22,30 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv
hasText(getResourceString(R.string.exchange_tokens_available_tokens_header))
}
fun tokenWithTitle(tokenTitle: String): KNode = child {
hasTestTag(TokenElementsTestTags.TOKEN_TITLE)
hasAnyDescendant(withText(tokenTitle))
useUnmergedTree = true
val searchIcon: KNode = child {
hasTestTag(AppBarWithSearchTestTags.SEARCH_ICON)
}
val searchTextField: KNode = child {
hasTestTag(AppBarWithSearchTestTags.TEXT_FIELD)
}
val noTokensFoundText: KNode = child {
hasText(getResourceString(R.string.express_token_list_empty_search))
}
fun tokenWithTitle(tokenTitle: String, availableForSwap: Boolean = true): KNode = child {
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
hasAnyDescendant(withText(tokenTitle))
if (!availableForSwap) {
hasAnyDescendant(
withText(
getResourceString(R.string.tokens_list_unavailable_to_swap_source_header)
)
)
}
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) =

View file

@ -2,6 +2,7 @@ package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
@ -33,6 +34,12 @@ class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNode
hasTestTag(SelectNetworkFeeBottomSheetTestTags.LEARN_MORE_TEXT)
useUnmergedTree = true
}
val applyButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_apply))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) =

View file

@ -43,6 +43,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
val feeAmount: KNode = child {
hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT))
useUnmergedTree = true
}
val receiveAmountShimmer: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER)
}
@ -71,6 +76,43 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
fun unableToCoverFeeNotificationTitle(networkName: String): KNode = child {
hasTestTag(NotificationTestTags.TITLE)
hasText(
getResourceString(
R.string.warning_express_not_enough_fee_for_token_tx_title,
networkName
)
)
useUnmergedTree = true
}
fun unableToCoverFeeNotificationText(currencyName: String, currencySymbol: String): KNode = child {
hasTestTag(NotificationTestTags.MESSAGE)
hasText(
getResourceString(
R.string.warning_express_not_enough_fee_for_token_tx_description,
currencyName,
currencySymbol
)
)
useUnmergedTree = true
}
fun unableToCoverFeeNotificationIcon(networkName: String): KNode = child {
hasTestTag(NotificationTestTags.ICON)
hasAnySibling(withTestTag(NotificationTestTags.TITLE))
hasAnySibling(
withText(
getResourceString(
R.string.warning_express_not_enough_fee_for_token_tx_title,
networkName,
)
)
)
useUnmergedTree = true
}
val refreshButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.warning_button_refresh))
@ -95,15 +137,30 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
useUnmergedTree = true
}
val insufficientFundsErrorTitle: KNode = child {
hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE)
hasText(getResourceString(R.string.swapping_insufficient_funds))
useUnmergedTree = true
}
val receiveFiatAmount: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT)
}
val receiveFiatAmountWithPriceImpactWarning: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING)
}
val receiveFiatAmountInformationIcon: KNode = child {
hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON)
useUnmergedTree = true
}
val swapFiatAmount: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT)
}
val changeTokenIcon: KNode = child {
val selectTokenIcon: KNode = child {
hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON)
}

View file

@ -64,7 +64,7 @@ class DetailsTest : BaseTestCase() {
fun wallet2DetailsTest() =
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
openMainScreen(productType = ProductType.Wallet2)
}
onTopBar {
step("Open wallet details") {

View file

@ -6,11 +6,12 @@ import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.redux.StateDialog
import com.tangem.scenarios.*
import com.tangem.scenarios.checkFailedTransactionDialog
import com.tangem.scenarios.checkScanWarningDialog
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
@ -96,7 +97,7 @@ class FeedbackTest : BaseTestCase() {
step("Click 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Assert sanding text is displayed") {
step("Assert sеnding text is displayed") {
onSendConfirmScreen { sendingText.assertIsDisplayed() }
}
step("Click 'Send' button") {
@ -163,39 +164,4 @@ class FeedbackTest : BaseTestCase() {
}
}
}
@AllureId("3986")
@DisplayName("Send feedback: from scan already used wallet alert dialog")
@Test
fun sendFeedbackAfterScanAlreadyUsedWalletTest() {
val gmailText = "Welcome to Gmail"
setupHooks(
additionalAfterSection = {
device.uiDevice.pressBack()
}
).run {
step("Set mocks for Wallet2") {
MockProvider.setMocks(ProductType.Wallet2)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Scan card or ring' button") {
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
}
step("Check 'Already used Wallet' dialog") {
checkAlreadyUsedWalletDialog()
}
step("Click on 'Request support' button") {
AlreadyUsedWalletDialogPageObject { requestSupportButton.click() }
}
step("Assert 'Gmail' app is open") {
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
}
}
}
}

View file

@ -30,10 +30,7 @@ class OnboardingTest : BaseTestCase() {
@Test
fun shibaBackupScreenTest() {
setupHooks().run {
scanCard(
mockContent = ShibaNoBackupMockContent,
alreadyActivatedDialogIsShown = true
)
scanCard(mockContent = ShibaNoBackupMockContent)
checkBackupScreen()
}
}
@ -54,10 +51,7 @@ class OnboardingTest : BaseTestCase() {
@Test
fun wallet2BackupScreenTest() {
setupHooks().run {
scanCard(
mockContent = Wallet2NoBackupMockContent,
alreadyActivatedDialogIsShown = true
)
scanCard(mockContent = Wallet2NoBackupMockContent)
checkBackupScreen()
}
}

View file

@ -64,7 +64,7 @@ class ResetCardTest : BaseTestCase() {
fun resetWallet2CardWithBackupTest() {
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
openMainScreen(productType = ProductType.Wallet2)
}
step("Open 'Device settings' screen") {
openDeviceSettingsScreen()

View file

@ -73,7 +73,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on $card") {
openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
openMainScreen(mockContent = cardType)
}
step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(
@ -119,7 +119,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on '$card' card") {
openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
openMainScreen(mockContent = cardType)
}
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(devicesCount, cardName)
@ -138,7 +138,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on '$ring'") {
openMainScreen(productType = cardType, alreadyActivatedDialogIsShown = true)
openMainScreen(productType = cardType)
}
step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(devicesCount, cardName)
@ -175,7 +175,7 @@ class ScanCardTest : BaseTestCase() {
setupHooks().run {
step("Open 'Main Screen' on '$card' card") {
openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
openMainScreen(mockContent = cardType)
}
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
checkMultiCurrencyMainScreen(devicesCount, cardName)

View file

@ -1,9 +1,7 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.scenarios.openMainScreen
@ -22,30 +20,32 @@ class SendTest : BaseTestCase() {
@DisplayName("Send: check fee notification")
@Test
fun checkFeeNotificationTest() {
val currencyName = "POL (ex-MATIC)"
val feeCurrencyName = "Ethereum"
val feeCurrencySymbol = "ETH"
val scenarioName = "eth_network_balance"
val scenarioState = "Empty"
val currencyName = "USDC"
val feeCurrencyName = "Solana"
val feeCurrencySymbol = "SOL"
val balanceScenarioName = "solana_balance"
val tokensScenarioName = "user_tokens_api"
val balanceState = "Empty"
val tokensState = "SolanaUSDC"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
resetWireMockScenarioState(balanceScenarioName)
resetWireMockScenarioState(tokensScenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
step("Set WireMock scenario: '$tokensScenarioName' to state: '$tokensState'") {
setWireMockScenarioState(scenarioName = tokensScenarioName, state = tokensState)
}
step("Set WireMock scenario: '$balanceScenarioName' to state: '$balanceState'") {
setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: $currencyName") {
onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() }
}

View file

@ -75,7 +75,7 @@ class WarningTest : BaseTestCase() {
}
).run {
step("Open 'Main' screen") {
openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent, alreadyActivatedDialogIsShown = true)
openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent)
}
step("Assert 'Seed phrase' notification icon is displayed") {
onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() }

View file

@ -23,7 +23,7 @@ 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 depositAmount = "KSM 0.000003333"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(

View file

@ -23,7 +23,7 @@ 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 depositAmount = "DOT 0.01"
private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title)
private val warningMessage = getResourceString(

View file

@ -0,0 +1,182 @@
package com.tangem.tests.swap
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.SwapEntryPoint
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSwapScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
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 SwapChooseTokenScreenTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("8505")
@DisplayName("Swap: check available to swap tokens list")
@Test
fun checkAvailableToSwapTokensListTest() {
val tokenTitle = "Polygon"
val inputAmount = "100"
val ethereum = "Ethereum"
val polExMatic = "POL (ex-MATIC)"
val bitcoin = "Bitcoin"
val scenarioState = "CustomTokenAndJesusAdded"
val jesusCoin = "Jesus Coin"
val salam = "Salam"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert title: '$tokenTitle' is displayed") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
}
step("Assert '$ethereum' is displayed") {
onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsDisplayed() }
}
step("Assert '$polExMatic' is displayed") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() }
}
step("Assert '$bitcoin' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(bitcoin).assertIsNotDisplayed() }
}
step("Assert '$jesusCoin' is displayed and unavailable for swap") {
onSwapChooseTokenScreen {
tokenWithTitle(
tokenTitle = jesusCoin,
availableForSwap = false
).assertIsDisplayed()
}
}
step("Assert custom token without backend id '$salam' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(salam).assertIsNotDisplayed() }
}
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("8506")
@DisplayName("Swap: check search on choose swap token screen")
@Test
fun checkSearchOnSwapChooseTokenScreenTest() {
val tokenTitle = "Polygon"
val inputAmount = "100"
val ethereum = "Ethereum"
val polExMatic = "POL (ex-MATIC)"
val polExMaticSymbol = "POL"
val invalidSearchText = "f"
val validSearchText = "pol"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert title: '$tokenTitle' is displayed") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { selectTokenIcon.performClick() }
}
step("Click on 'Search' icon") {
onSwapChooseTokenScreen { searchIcon.performClick() }
}
step("Click on 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performClick() }
}
step("Type invalid search text: '$invalidSearchText' in 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performTextReplacement(invalidSearchText) }
}
step("Assert '$ethereum' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() }
}
step("Assert '$polExMatic' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsNotDisplayed() }
}
step("Press 'Delete' button") {
device.uiDevice.pressDelete()
}
step("Type valid search text: '$validSearchText' in 'Search' text field") {
onSwapChooseTokenScreen { searchTextField.performTextReplacement(validSearchText) }
}
step("Assert '$ethereum' is not displayed") {
onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() }
}
step("Assert '$polExMatic' is displayed") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() }
}
step("Select new receive token: $polExMatic") {
onSwapChooseTokenScreen { tokenWithTitle(polExMatic).performClick() }
}
step("Assert new receive token symbol: '$polExMaticSymbol' is displayed") {
onSwapTokenScreen { receiveTokenSymbol(polExMaticSymbol).assertIsDisplayed() }
}
}
}
}

View file

@ -4,16 +4,16 @@ import androidx.compose.ui.test.hasText
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.resetWireMockScenarios
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.SwapEntryPoint
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSwapScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.scenarios.*
import com.tangem.screens.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
@ -266,6 +266,9 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("2828")
@DisplayName("Swap: network fee")
@Test
@ -317,6 +320,9 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("575")
@DisplayName("Swap: check UI")
@Test
@ -354,7 +360,7 @@ class SwapTokenScreenTest : BaseTestCase() {
onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() }
}
step("Click on 'Select token' icon") {
onSwapTokenScreen { changeTokenIcon.performClick() }
onSwapTokenScreen { selectTokenIcon.performClick() }
}
step("Select new receive token: $newReceiveToken") {
onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() }
@ -401,6 +407,9 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("5162")
@DisplayName("Swap: check swap tokens switch")
@Test
@ -444,4 +453,293 @@ class SwapTokenScreenTest : BaseTestCase() {
}
}
}
@AllureId("573")
@DisplayName("Swap: check 'Swap' button availability")
@Test
fun checkSwapButtonAvailabilityTest() {
val polygon = "Polygon"
val bitcoin = "Bitcoin"
val salam = "Salam"
val jesusCoin = "Jesus Coin"
val myria = "Myria"
val scenarioState = "CustomTokenAndJesusAdded"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$polygon'") {
onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() }
}
step("Assert 'Swap' button is not dimmed. Swap available") {
onTokenDetailsScreen { swapButton().assertIsDimmed(false) }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Click on token with name: '$bitcoin'. Swap unavailable") {
onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Click on unknown custom token with name: '$salam'. Swap unavailable") {
onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Swipe up") {
onMainScreen { tokenWithTitleAndAddress(jesusCoin).assertIsDisplayed() }
waitForIdle()
swipeVertical(SwipeDirection.UP)
}
step("Click on token with name: '$jesusCoin' in 'Ethereum' network. Swap unavailable") {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(jesusCoin).clickWithAssertion() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
}
step("Press 'Back' button") {
device.uiDevice.pressBack()
}
step("Click on custom token with 'exchangeAvailable=true': '$myria'. Swap unavailable") {
onMainScreen { tokenWithTitleAndAddress(myria).clickWithAssertion() }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertIsDimmed(true) }
}
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("583")
@DisplayName("Swap: check switch fee type (enable to cover 'Market' and 'Fast' fee)")
@Test
fun enableToCoverMarketAndFastFeeTest() {
val tokenName = "Ethereum"
val inputAmount = "0.99"
val market = "Market"
val fast = "Fast"
val marketFeeAmount = "$1.12"
val fastFeeAmount = "$1.43"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Select '$market' fee type") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount)
}
}
step("Select '$fast' fee type") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount)
}
}
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("8536")
@DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)")
@Test
fun unableToCoverMarketAndFastFeeTest() {
val tokenName = "POL (ex-MATIC)"
val inputAmount = "0.0001"
val market = "Market"
val fast = "Fast"
val marketFeeAmount = "$18,932"
val fastFeeAmount = "$24,139"
val scenarioName = "eth_network_balance"
val scenarioState = "LessThanDollar"
val networkName = "Ethereum"
val currencySymbol = "ETH"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Select '$market' fee type") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount)
}
}
step("Check 'Unable to cover '$networkName' fee notification") {
chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
}
step("Assert 'Swap' button is disabled") {
onSwapTokenScreen { swapButton.assertIsNotEnabled() }
}
step("Select '$fast' fee type") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount)
}
}
step("Check 'Unable to cover '$networkName' fee notification") {
chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
}
step("Assert 'Swap' button is disabled") {
onSwapTokenScreen { swapButton.assertIsNotEnabled() }
}
}
}
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("8537")
@DisplayName("Swap: check switch fee type (unable to cover 'Fast' fee)")
@Test
fun unableToCoverFastFeeTest() {
val tokenName = "POL (ex-MATIC)"
val inputAmount = "3000"
val fastFeeType = "Fast"
val fastFeeAmount = "$2,"
val marketFeeType = "Market"
val marketFeeAmount = "$1."
val scenarioName = "eth_fee_history"
val scenarioState = "UnableToCoverFastFee"
val networkName = "Ethereum"
val currencySymbol = "ETH"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(scenarioName)
}
).run {
step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") {
setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenName'") {
onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert 'Swap' button is enabled") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
onSwapTokenScreen { swapButton.assertIsEnabled() }
}
}
step("Select '$fastFeeType' fee type") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount)
}
}
step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") {
onSwapTokenScreen { feeAmount.assertTextContains(fastFeeAmount, substring = true) }
}
step("Check 'Unable to cover '$networkName' fee notification") {
chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
}
step("Assert 'Swap' button is disabled") {
onSwapTokenScreen { swapButton.assertIsNotEnabled() }
}
step("Select '$marketFeeType' fee type") {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount)
}
}
step("Assert fee amount is equal to '$marketFeeType' fee:'$marketFeeAmount'") {
onSwapTokenScreen { feeAmount.assertTextContains(marketFeeAmount, substring = true) }
}
step("Assert 'Swap' button is enabled") {
waitForIdle()
onSwapTokenScreen { swapButton.assertIsEnabled() }
}
}
}
}

View file

@ -0,0 +1,314 @@
package com.tangem.tests.swap
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.*
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.SwapEntryPoint
import com.tangem.scenarios.chackUnableToCoverFeeNotification
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.openSwapScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
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 SwapTokenScreenWarningsTest : BaseTestCase() {
@ApiEnv(
ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD)
)
@AllureId("580")
@DisplayName("Swap: check 'Insufficient funds' warning")
@Test
fun checkSwapInsufficientFundsWarningTest() {
val tokenTitle = "Polygon"
val inputAmount = "1000"
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert title: '$tokenTitle' is displayed") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert 'Insufficient funds' error is displayed") {
waitForIdle()
onSwapTokenScreen { insufficientFundsErrorTitle.assertIsDisplayed() }
}
}
}
@AllureId("8502")
@DisplayName("Swap: check 'Unable to cover network fee' warning")
@Test
fun checkUnableToCoverBlockchainFeeWarningTest() {
val tokenTitle = "USDC"
val inputAmount = "1000"
val tokensScenarioState = "SolanaUSDC"
val balanceScenarioName = "solana_balance"
val balanceScenarioState = "Empty"
val networkName = "Solana"
val currencySymbol = "SOL"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(balanceScenarioState)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
}
step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") {
setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert title: '$tokenTitle' is displayed") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Check 'Unable to cover '$networkName' fee notification") {
chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol)
}
step("Assert 'Unable to cover '$networkName' fee notification icon is displayed'") {
onSwapTokenScreen { unableToCoverFeeNotificationIcon(networkName).assertIsDisplayed() }
}
step("Assert 'Swap' button is disabled") {
onSwapTokenScreen { swapButton.assertIsNotEnabled() }
}
}
}
@AllureId("8503")
@DisplayName("Swap: check 'High price impact' warning on CEX")
@Test
fun checkHighPriceImpactWarningCEXTest() {
val tokenTitle = "USDC"
val inputAmount = "100"
val currencySymbol = "SOL"
val slippagePercent = "5%"
val tokensScenarioState = "SolanaUSDC"
val exchangeQuoteScenarioName = "exchange_quote_solana"
val exchangeQuoteScenarioState = "HighPriceImpact"
val dialogTitle = getResourceString(R.string.swapping_alert_title)
val dialogText = getResourceString(
R.string.swapping_alert_cex_description_with_slippage,
currencySymbol,
slippagePercent
)
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
resetWireMockScenarioState(exchangeQuoteScenarioName)
}
).run {
step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") {
setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState)
}
step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") {
setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState)
}
step("Set WireMock scenario: '$exchangeQuoteScenarioName' to state: $exchangeQuoteScenarioState") {
setWireMockScenarioState(
scenarioName = exchangeQuoteScenarioName,
state = exchangeQuoteScenarioState
)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert title: '$tokenTitle' is displayed") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert fiat amount with warning is displayed") {
onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) }
}
step("Assert receive amount information icon is displayed") {
onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() }
}
step("Click on receive amount information icon") {
onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() }
}
step("Assert information dialog is displayed") {
onDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert information dialog title is displayed") {
onDialog { title.assertTextEquals(dialogTitle) }
}
step("Assert information dialog text for CEX is displayed") {
onDialog { text.assertTextEquals(dialogText) }
}
step("Assert dialog 'OK' button is displayed") {
onDialog { okButton.assertIsDisplayed() }
}
}
}
@AllureId("8504")
@DisplayName("Swap: check 'High price impact' warning on DEX")
@Test
fun checkHighPriceImpactWarningDEXTest() {
val tokenTitle = "Polygon"
val inputAmount = "1000"
val slippagePercent = "3.5%"
val dialogTitle = getResourceString(R.string.swapping_alert_title)
val highPriceImpactDescription = getResourceString(R.string.swapping_high_price_impact_description)
val swappingAlertDEXDescription = getResourceString(R.string.swapping_alert_dex_description)
val swappingAlertDEXDescriptionWithSlippage = getResourceString(
R.string.swapping_alert_dex_description_with_slippage,
slippagePercent
)
val pairsToScenarioName = "polygon_pos_to_pairs"
val pairsFromScenarioName = "polygon_pos_from_pairs"
val scenarioState = "DexProvider"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(pairsToScenarioName)
resetWireMockScenarioState(pairsFromScenarioName)
}
).run {
step("Set WireMock scenario: '$pairsToScenarioName' to state: $scenarioState") {
setWireMockScenarioState(scenarioName = pairsToScenarioName, state = scenarioState)
}
step("Set WireMock scenario: '$pairsFromScenarioName' to state: $scenarioState") {
setWireMockScenarioState(scenarioName = pairsFromScenarioName, state = scenarioState)
}
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Click on token with name: '$tokenTitle'") {
onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() }
}
step("Assert title: '$tokenTitle' is displayed") {
onTokenDetailsScreen { title.assertIsDisplayed() }
}
step("Open 'Swap' screen") {
openSwapScreen(from = SwapEntryPoint.TokenDetails)
}
step("Assert 'You swap' block is displayed") {
onSwapTokenScreen { youSwapBlock.assertIsDisplayed() }
}
step("Input swap amount = '$inputAmount'") {
waitForIdle()
onSwapTokenScreen {
textInput.clickWithAssertion()
textInput.performTextReplacement(inputAmount)
}
}
step("Assert fiat amount with warning is displayed") {
onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) }
}
step("Assert receive amount information icon is displayed") {
onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() }
}
step("Click on receive amount information icon") {
onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() }
}
step("Assert information dialog is displayed") {
onDialog { dialogContainer.assertIsDisplayed() }
}
step("Assert information dialog title is displayed") {
onDialog { title.assertTextEquals(dialogTitle) }
}
step("Assert information dialog text for DEX is displayed") {
onDialog {
text.assertTextContains(highPriceImpactDescription, substring = true)
text.assertTextContains(swappingAlertDEXDescription, substring = true)
text.assertTextContains(swappingAlertDEXDescriptionWithSlippage, substring = true)
}
}
step("Assert dialog 'OK' button is displayed") {
onDialog { okButton.assertIsDisplayed() }
}
step("Click on 'OK' button") {
onDialog { okButton.performClick() }
}
}
}
}

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="146"
android:viewportHeight="146">
<!-- Tangem logo -->
<group
android:scaleX="0.33"
android:scaleY="0.33"
android:translateX="48.91"
android:translateY="48.91">
<path
android:fillColor="#1E1E1E"
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
</group>
<!-- Colored triangle in bottom-right corner -->
<path
android:fillColor="#4CAF50"
android:pathData="M146,20 L146,146 L20,146 Z" />
<!-- DEBUG text (rotated 45 degrees) -->
<group
android:rotation="-45"
android:pivotX="91.0"
android:pivotY="91.0">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M67.0,84.7 L70.6,84.7 Q71.9,84.7,72.5,84.9 Q73.4,85.1,74.0,85.8 Q74.7,86.4,75.0,87.4 Q75.3,88.3,75.3,89.7 Q75.3,90.9,75.0,91.8 Q74.6,92.9,73.9,93.6 Q73.4,94.1,72.5,94.4 Q71.9,94.6,70.8,94.6 L67.0,94.6 Z M69.0,86.3 L69.0,92.9 L70.5,92.9 Q71.3,92.9,71.7,92.8 Q72.2,92.7,72.5,92.4 Q72.8,92.1,73.0,91.4 Q73.2,90.8,73.2,89.6 Q73.2,88.5,73.0,87.9 Q72.8,87.3,72.5,86.9 Q72.1,86.6,71.5,86.4 Q71.1,86.3,69.9,86.3 Z M77.0,94.6 L77.0,84.7 L84.4,84.7 L84.4,86.3 L79.0,86.3 L79.0,88.5 L84.0,88.5 L84.0,90.2 L79.0,90.2 L79.0,92.9 L84.5,92.9 L84.5,94.6 Z M86.2,84.7 L90.2,84.7 Q91.4,84.7,92.0,84.8 Q92.5,84.9,93.0,85.2 Q93.5,85.5,93.8,86.0 Q94.1,86.5,94.1,87.2 Q94.1,87.9,93.7,88.4 Q93.3,89.0,92.7,89.3 Q93.6,89.6,94.1,90.2 Q94.6,90.9,94.6,91.7 Q94.6,92.4,94.2,93.0 Q93.9,93.7,93.4,94.0 Q92.8,94.4,92.0,94.5 Q91.5,94.6,89.6,94.6 L86.2,94.6 Z M88.2,86.3 L88.2,88.6 L89.6,88.6 Q90.7,88.6,91.0,88.6 Q91.5,88.5,91.8,88.2 Q92.1,87.9,92.1,87.4 Q92.1,87.0,91.9,86.7 Q91.6,86.4,91.1,86.4 Q90.8,86.3,89.4,86.3 Z M88.2,90.3 L88.2,92.9 L90.1,92.9 Q91.2,92.9,91.5,92.9 Q91.9,92.8,92.2,92.5 Q92.5,92.1,92.5,91.6 Q92.5,91.2,92.3,90.9 Q92.0,90.5,91.6,90.4 Q91.2,90.3,89.9,90.3 Z M96.2,84.7 L98.2,84.7 L98.2,90.0 Q98.2,91.3,98.3,91.7 Q98.4,92.3,98.9,92.7 Q99.4,93.0,100.2,93.0 Q101.1,93.0,101.5,92.7 Q102.0,92.3,102.0,91.8 Q102.1,91.3,102.1,90.2 L102.1,84.7 L104.1,84.7 L104.1,89.9 Q104.1,91.7,104.0,92.4 Q103.8,93.1,103.4,93.6 Q102.9,94.2,102.2,94.5 Q101.5,94.8,100.3,94.8 Q98.9,94.8,98.1,94.4 Q97.4,94.1,97.0,93.6 Q96.6,93.0,96.4,92.5 Q96.2,91.6,96.2,90.0 Z M110.9,90.9 L110.9,89.3 L115.2,89.3 L115.2,93.2 Q114.5,93.8,113.4,94.3 Q112.2,94.8,110.9,94.8 Q109.4,94.8,108.2,94.1 Q107.1,93.4,106.5,92.2 Q105.9,91.0,105.9,89.6 Q105.9,88.0,106.5,86.8 Q107.2,85.6,108.5,85.0 Q109.4,84.5,110.8,84.5 Q112.7,84.5,113.7,85.3 Q114.7,86.0,115.0,87.4 L113.1,87.8 Q112.8,87.1,112.3,86.6 Q111.7,86.2,110.8,86.2 Q109.5,86.2,108.7,87.0 Q108.0,87.9,108.0,89.5 Q108.0,91.3,108.7,92.2 Q109.5,93.0,110.8,93.0 Q111.4,93.0,112.1,92.8 Q112.7,92.5,113.2,92.2 L113.2,90.9 Z" />
</group>
</vector>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="146"
android:viewportHeight="146">
<!-- Tangem logo -->
<group
android:scaleX="0.33"
android:scaleY="0.33"
android:translateX="48.91"
android:translateY="48.91">
<path
android:fillColor="#1E1E1E"
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
</group>
<!-- Colored triangle in bottom-right corner -->
<path
android:fillColor="#2196F3"
android:pathData="M146,20 L146,146 L20,146 Z" />
<!-- INTERNAL text (rotated 45 degrees) -->
<group
android:rotation="-45"
android:pivotX="91.0"
android:pivotY="91.0">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M56.9,94.6 L56.9,84.7 L58.9,84.7 L58.9,94.6 Z M60.9,94.6 L60.9,84.7 L62.8,84.7 L66.9,91.3 L66.9,84.7 L68.7,84.7 L68.7,94.6 L66.7,94.6 L62.7,88.1 L62.7,94.6 Z M73.1,94.6 L73.1,86.3 L70.1,86.3 L70.1,84.7 L78.0,84.7 L78.0,86.3 L75.1,86.3 L75.1,94.6 Z M79.3,94.6 L79.3,84.7 L86.7,84.7 L86.7,86.3 L81.3,86.3 L81.3,88.5 L86.3,88.5 L86.3,90.2 L81.3,90.2 L81.3,92.9 L86.9,92.9 L86.9,94.6 Z M88.6,94.6 L88.6,84.7 L92.8,84.7 Q94.4,84.7,95.1,84.9 Q95.8,85.2,96.2,85.9 Q96.7,86.6,96.7,87.4 Q96.7,88.6,96.0,89.3 Q95.4,90.0,94.0,90.2 Q94.7,90.6,95.1,91.0 Q95.5,91.5,96.3,92.7 L97.5,94.6 L95.1,94.6 L93.6,92.4 Q92.9,91.3,92.6,91.0 Q92.3,90.7,92.0,90.6 Q91.7,90.4,91.0,90.4 L90.6,90.4 L90.6,94.6 Z M90.6,88.9 L92.0,88.9 Q93.5,88.9,93.8,88.7 Q94.2,88.6,94.4,88.3 Q94.6,88.0,94.6,87.6 Q94.6,87.1,94.3,86.8 Q94.1,86.5,93.6,86.4 Q93.3,86.3,92.1,86.3 L90.6,86.3 Z M98.6,94.6 L98.6,84.7 L100.5,84.7 L104.6,91.3 L104.6,84.7 L106.4,84.7 L106.4,94.6 L104.4,94.6 L100.4,88.1 L100.4,94.6 Z M117.5,94.6 L115.3,94.6 L114.5,92.3 L110.5,92.3 L109.7,94.6 L107.5,94.6 L111.4,84.7 L113.5,84.7 Z M113.8,90.7 L112.4,87.0 L111.1,90.7 Z M118.6,94.6 L118.6,84.8 L120.6,84.8 L120.6,92.9 L125.6,92.9 L125.6,94.6 Z" />
</group>
</vector>

View file

@ -19,7 +19,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -48,7 +48,6 @@ import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@ -58,12 +57,12 @@ import dagger.hilt.components.SingletonComponent
@Suppress("TooManyFunctions")
interface ApplicationEntryPoint {
fun getEnvironmentConfigStorage(): EnvironmentConfigStorage
fun getAppStateHolder(): AppStateHolder
fun getIssuersConfigStorage(): IssuersConfigStorage
fun getEnvironmentConfig(): EnvironmentConfig
fun getFeatureTogglesManager(): FeatureTogglesManager
fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager
@ -120,8 +119,6 @@ interface ApplicationEntryPoint {
fun getOnboardingRepository(): OnboardingRepository
fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider
fun getExcludedBlockchains(): ExcludedBlockchains
fun getAppLogsStore(): AppLogsStore

View file

@ -44,7 +44,6 @@ import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tester.api.TesterMenuLauncher
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
@ -161,9 +160,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase
@Inject
internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles
private val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>

View file

@ -36,7 +36,6 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -67,29 +66,26 @@ import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient
import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import timber.log.Timber
lateinit var store: Store<AppState>
val foregroundActivityObserver = ForegroundActivityObserver
internal lateinit var derivationsFinder: DerivationsFinder
open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
@ -100,12 +96,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val appStateHolder: AppStateHolder
get() = entryPoint.getAppStateHolder()
private val environmentConfigStorage: EnvironmentConfigStorage
get() = entryPoint.getEnvironmentConfigStorage()
private val issuersConfigStorage: IssuersConfigStorage
get() = entryPoint.getIssuersConfigStorage()
private val environmentConfig: EnvironmentConfig
get() = entryPoint.getEnvironmentConfig()
private val featureTogglesManager: FeatureTogglesManager
get() = entryPoint.getFeatureTogglesManager()
@ -190,9 +186,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val onboardingRepository: OnboardingRepository
get() = entryPoint.getOnboardingRepository()
private val dispatchers: CoroutineDispatcherProvider
get() = entryPoint.getCoroutineDispatcherProvider()
private val excludedBlockchains: ExcludedBlockchains
get() = entryPoint.getExcludedBlockchains()
@ -312,9 +305,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
Timber.i(excludedBlockchainsManager.toString())
}
runBlocking {
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
initWithConfigDependency(environmentConfig = environmentConfig)
abTestsManager.init()
@ -348,15 +339,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
)
}
derivationsFinder = DerivationsFinder(
userTokensResponseStore = userTokensResponseStore,
dispatchers = dispatchers,
)
appStateHolder.mainStore = store
wcInitializeUseCase.init(
projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId,
projectId = environmentConfig.walletConnectProjectId,
)
}
@ -387,7 +373,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
shareManager = shareManager,
appRouter = appRouter,
transactionSignerFactory = transactionSignerFactory,
environmentConfigStorage = environmentConfigStorage,
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
onboardingRepository = onboardingRepository,
excludedBlockchains = excludedBlockchains,
@ -426,6 +411,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory))
factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder())
factory.addFilter(oneTimeEventFilter)
factory.addFilter(AppsFlyerEventFilter())

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.analytics.handlers.customerio
import com.tangem.core.analytics.api.UserIdHolder
/**
* Client interface for Customer.io SDK operations.
*/
interface CustomerIoAnalyticsClient : UserIdHolder

View file

@ -0,0 +1,51 @@
package com.tangem.tap.common.analytics.handlers.customerio
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.AnalyticsUserIdHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
/**
* Customer.io analytics handler.
*/
class CustomerIoAnalyticsHandler(
private val client: CustomerIoAnalyticsClient,
) : AnalyticsHandler, AnalyticsUserIdHandler {
override fun id(): String = ID
override fun setUserId(userId: String) {
client.setUserId(userId)
}
override fun clearUserId() {
client.clearUserId()
}
override fun send(event: AnalyticsEvent) {
// No-op: product events are not sent to Customer.io.
// Triggers are configured to come from Amplitude directly.
}
companion object {
const val ID = "CustomerIO"
}
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? {
val cdpApiKey = data.config.customerIoCdpApiKey
return if (data.logConfig.isCustomerIoLogEnabled) {
CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
} else if (!cdpApiKey.isNullOrBlank()) {
CustomerIoAnalyticsHandler(
client = CustomerIoClient(
application = data.application,
cdpApiKey = cdpApiKey,
),
)
} else {
null
}
}
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.tap.common.analytics.handlers.customerio
import android.app.Application
import io.customer.messagingpush.ModuleMessagingPushFCM
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOBuilder
import timber.log.Timber
/**
* Real Customer.io SDK client.
*
* Initializes the SDK with the given CDP API key and provides:
* - User identification (identify / clearIdentify)
*
* Auto-tracking of application lifecycle events is disabled since it is not needed.
* Auto-tracking of screen views is disabled.
*/
internal class CustomerIoClient(
application: Application,
cdpApiKey: String,
) : CustomerIoAnalyticsClient {
init {
CustomerIOBuilder(
applicationContext = application,
cdpApiKey = cdpApiKey,
)
.trackApplicationLifecycleEvents(false)
.autoTrackActivityScreens(false)
.addCustomerIOModule(ModuleMessagingPushFCM())
.build()
Timber.d("CustomerIO SDK initialized")
}
override fun setUserId(userId: String) {
CustomerIO.instance().identify(userId = userId)
}
override fun clearUserId() {
CustomerIO.instance().clearIdentify()
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.common.analytics.handlers.customerio
import timber.log.Timber
/**
* Log client for Customer.io (used in debug mode).
*
* Logs all operations to Timber instead of sending them to Customer.io.
*/
internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
private var userId: String? = null
override fun setUserId(userId: String) {
this.userId = userId
Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
}
override fun clearUserId() {
Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
this.userId = null
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import timber.log.Timber
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
@ -15,11 +16,15 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token)
}
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)
CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message)
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID

View file

@ -6,7 +6,6 @@ import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import org.rekotlin.Middleware
@ -29,7 +28,6 @@ data class AppState(
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,
TradeCryptoMiddleware.middleware,
)
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.data
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.offramp.repository.OfframpRepository
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.network.exchangeServices.SellService
/**
* Default implementation of [OfframpRepository]
*
* @property sellService sell service for getting offramp URL
*/
internal class DefaultOfframpRepository(
private val sellService: SellService,
) : OfframpRepository {
override fun getOfframpUrl(
cryptoCurrency: CryptoCurrency,
fiatCurrencyCode: String,
walletAddress: String,
): String? {
return sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di
import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
@ -69,14 +69,14 @@ internal object ActivityModule {
@Provides
@Singleton
fun provideExchangeService(
environmentConfigStorage: EnvironmentConfigStorage,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
moonPayApi: MoonPayApi,
environmentConfig: EnvironmentConfig,
): SellService {
return MoonPayService(
api = moonPayApi,
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey },
secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey },
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}

View file

@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler
@ -40,6 +41,7 @@ internal class TangemSdkManagerModule {
appFinisher: AppFinisher,
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
analyticsExceptionHandler: AnalyticsExceptionHandler,
blockchainToDeriveFinder: BlockchainToDeriveFinder,
dispatchers: CoroutineDispatcherProvider,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
@ -56,6 +58,7 @@ internal class TangemSdkManagerModule {
appFinisher = appFinisher,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
analyticsExceptionHandler = analyticsExceptionHandler,
blockchainToDeriveFinder = blockchainToDeriveFinder,
dispatchers = dispatchers,
)
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
@ -97,12 +96,10 @@ internal object AccountDomainModule {
fun provideIsAccountsModeEnabledUseCase(
userWalletsListRepository: UserWalletsListRepository,
accountsCRUDRepository: AccountsCRUDRepository,
accountsFeatureToggles: AccountsFeatureToggles,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(
userWalletsListRepository = userWalletsListRepository,
crudRepository = accountsCRUDRepository,
accountsFeatureToggles = accountsFeatureToggles,
)
}

View file

@ -1,9 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.earn.usecase.*
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -19,15 +19,15 @@ object EarnDomainModule {
}
@Provides
fun provideManageEarnNetworksUseCase(
fun provideGetEarnNetworksUseCase(
earnRepository: EarnRepository,
multiAccountListSupplier: MultiAccountListSupplier,
userWalletsListRepository: UserWalletsListRepository,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
): GetEarnNetworksUseCase {
return GetEarnNetworksUseCase(
earnRepository = earnRepository,
multiAccountListSupplier = multiAccountListSupplier,
userWalletsListRepository = userWalletsListRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
)
}

View file

@ -3,20 +3,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -57,40 +48,6 @@ internal object ManageTokensDomainModule {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideRemoveCustomManagedCryptoCurrencyUseCase(
customTokensRepository: CustomTokensRepository,
): RemoveCustomManagedCryptoCurrencyUseCase {
return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideSaveManagedTokensUseCase(
customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
derivationsRepository: DerivationsRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
derivationsRepository = derivationsRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)
}
@Provides
@Singleton
fun provideGetSupportedNetworksUseCase(

View file

@ -4,22 +4,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.staking.StakingIdFactory
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.derivations.DerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@ -66,32 +56,6 @@ object MarketsDomainModule {
return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier)
}
@Provides
@Singleton
fun provideSaveMarketTokensUseCase(
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
stakingIdFactory = stakingIdFactory,
parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default),
)
}
@Provides
@Singleton
fun provideGetTokenMarketCryptoCurrency(

View file

@ -1,6 +1,5 @@
package com.tangem.tap.di.domain
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.nft.*
@ -25,15 +24,11 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesGetNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
singleAccountListSupplier: SingleAccountListSupplier,
accountsFeatureToggles: AccountsFeatureToggles,
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
singleAccountListSupplier = singleAccountListSupplier,
accountsFeatureToggles = accountsFeatureToggles,
)
@Provides

View file

@ -1,9 +1,13 @@
package com.tangem.tap.di.domain
import com.tangem.domain.offramp.GetOfframpUrlUseCase
import com.tangem.domain.offramp.repository.OfframpRepository
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.data.DefaultOfframpRepository
import com.tangem.tap.network.exchangeServices.SellService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -275,4 +279,16 @@ internal object OnrampDomainModule {
promoRepository = promoRepository,
)
}
@Provides
@Singleton
fun provideOfframpRepository(sellService: SellService): OfframpRepository {
return DefaultOfframpRepository(sellService)
}
@Provides
@Singleton
fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase {
return GetOfframpUrlUseCase(offrampRepository)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
@ -19,7 +20,6 @@ import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
@ -39,28 +39,6 @@ import javax.inject.Singleton
@Suppress("TooManyFunctions", "LargeClass")
internal object TokensDomainModule {
@Provides
@Singleton
fun provideAddCryptoCurrenciesUseCase(
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
stakingIdFactory = stakingIdFactory,
)
}
@Provides
@Singleton
fun provideFetchPendingTransactionsUseCase(
@ -75,32 +53,6 @@ internal object TokensDomainModule {
return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager)
}
@Provides
@Singleton
fun provideGetTokenListUseCase(
currenciesRepository: CurrenciesRepository,
currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetTokenListUseCase {
return GetTokenListUseCase(
currenciesRepository = currenciesRepository,
currenciesStatusesOperations = currenciesStatusesOperations,
)
}
@Provides
@Singleton
fun provideRemoveCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
)
}
@Provides
@Singleton
fun provideGetCurrencyUseCase(
@ -113,20 +65,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository: CurrenciesRepository,
currencyStatusOperations: BaseCurrencyStatusOperations,
dispatchers: CoroutineDispatcherProvider,
): GetAllWalletsCryptoCurrencyStatusesUseCase {
return GetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository = currenciesRepository,
currencyStatusOperations = currencyStatusOperations,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideGetCurrencyWarningsUseCase(
@ -176,34 +114,6 @@ internal object TokensDomainModule {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
@Provides
@Singleton
fun provideToggleTokenListGroupingUseCase(
dispatchers: CoroutineDispatcherProvider,
): ToggleTokenListGroupingUseCase {
return ToggleTokenListGroupingUseCase(dispatchers)
}
@Provides
@Singleton
fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase {
return ToggleTokenListSortingUseCase(dispatchers)
}
@Provides
@Singleton
fun provideApplyTokenListSortingUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideGetCryptoCurrencyActionsUseCase(
@ -308,14 +218,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetWalletTotalBalanceUseCase(
currenciesStatusesOperations: BaseCurrencyStatusOperations,
): GetWalletTotalBalanceUseCase {
return GetWalletTotalBalanceUseCase(currenciesStatusesOperations)
}
@Provides
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
@ -350,7 +252,7 @@ internal object TokensDomainModule {
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
return CachedCurrenciesStatusesOperations(
return BaseCurrencyStatusOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
@ -363,12 +265,6 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository)
}
@Provides
@Singleton
fun provideWalletBalanceFetcher(
@ -378,6 +274,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
stakingIdFactory: StakingIdFactory,
dispatchers: CoroutineDispatcherProvider,
): WalletBalanceFetcher {
@ -388,6 +285,7 @@ internal object TokensDomainModule {
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
stakingIdFactory = stakingIdFactory,
dispatchers = dispatchers,
)

View file

@ -66,6 +66,12 @@ internal object WalletsDomainModule {
return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository)
}
@Provides
@Singleton
fun provideGetWalletIconUseCase(walletsRepository: WalletsRepository): GetWalletIconUseCase {
return GetWalletIconUseCase(walletsRepository = walletsRepository)
}
@Provides
@Singleton
fun providesGetSelectedWalletSyncUseCase(

View file

@ -53,11 +53,7 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.*
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
@ -85,6 +81,7 @@ internal class DefaultTangemSdkManager(
private val appFinisher: AppFinisher,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder,
dispatchers: CoroutineDispatcherProvider,
) : TangemSdkManager {
@ -173,7 +170,7 @@ internal class DefaultTangemSdkManager(
runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
card = null,
derivationsFinder = derivationsFinder,
blockchainToDeriveFinder = blockchainToDeriveFinder,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
visaCardScanHandler = visaCardScanHandler,
visaCoroutineScope = this,

View file

@ -0,0 +1,74 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.wallets.derivations.BlockchainToDerive
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.tap.features.demo.DemoHelper
import javax.inject.Inject
/**
* Finder of blockchains to derive.
* Returns only saved, default or demo blockchains without any additional logic
* (no cardano/ethereum additions or unnecessary blockchain removals).
*/
class BlockchainToDeriveFinder @Inject constructor(
private val walletAccountsFetcher: WalletAccountsFetcher,
) {
suspend fun find(card: CardDTO): Set<BlockchainToDerive> {
if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
val derivationStyle = card.derivationStyleProvider.getDerivationStyle()
val blockchains = getBlockchains(userWalletId).ifEmpty {
if (DemoHelper.isDemoCardId(card.cardId)) {
getDemoBlockchains(derivationStyle, card.cardId)
} else {
getDefaultBlockchains(derivationStyle)
}
}
return blockchains
}
private suspend fun getBlockchains(userWalletId: UserWalletId): Set<BlockchainToDerive> {
return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty()
.flatMap { accountDTO ->
accountDTO.tokens.orEmpty()
.filter { it.contractAddress == null }
}
.mapNotNull { coin ->
val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null
BlockchainToDerive(blockchain, derivationPath)
}
.toSet()
}
private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set<BlockchainToDerive> {
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
}
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set<BlockchainToDerive> {
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
}
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
derivationStyle: DerivationStyle?,
): Set<BlockchainToDerive> {
return mapNotNullTo(hashSetOf()) { blockchain ->
val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null
BlockchainToDerive(blockchain, derivationPath)
}
}
}

View file

@ -1,131 +0,0 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal data class BlockchainToDerive(
val blockchain: Blockchain,
val derivationPath: DerivationPath?,
)
// FIXME: May be move to DI, currently unnecessary
internal class DerivationsFinder(
private val userTokensResponseStore: UserTokensResponseStore,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun findBlockchainsToDerive(
card: CardDTO,
derivationStyleProvider: DerivationStyleProvider,
): Set<BlockchainToDerive> {
if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet()
val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet()
val derivationStyle = derivationStyleProvider.getDerivationStyle()
val blockchains = withContext(dispatchers.io) {
getBlockchains(userWalletId)
}.ifEmpty {
if (DemoHelper.isDemoCardId(card.cardId)) {
getDemoBlockchains(derivationStyle, card.cardId)
} else {
getDefaultBlockchains(derivationStyle)
}
}
// we should generate second key for cardano
// because cardano address generation for wallet2 requires keys from 2 derivations
// https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/
blockchains.addSecondCardanoDerivationIfPresent()
if (card.settings.isHDWalletAllowed) {
blockchains.addEthereumBlockchains(derivationStyle)
}
// pay attention to this
if (!card.hasOldStyleDerivation) {
blockchains.removeUnnecessaryBlockchains()
}
return blockchains
}
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens
?: return hashSetOf()
return responseTokens.asSequence()
.filter { it.contractAddress == null }
.mapNotNull { coin ->
val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null
val derivationPath = coin.derivationPath?.let(::DerivationPath)
BlockchainToDerive(blockchain, derivationPath)
}
.toMutableSet()
}
// TODO: Move to user wallet config
private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet<BlockchainToDerive> {
return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle)
}
// TODO: Move to user wallet config
private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet<BlockchainToDerive> {
val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle)
}
}
private fun MutableSet<BlockchainToDerive>.addEthereumBlockchains(derivationStyle: DerivationStyle?) {
val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet)
.mapToBlockchainsWithDerivations(derivationStyle)
addAll(ethereumBlockchains)
}
private fun MutableSet<BlockchainToDerive>.removeUnnecessaryBlockchains() {
val unnecessaryBlockchains = listOf(
Blockchain.BSC, Blockchain.BSCTestnet,
Blockchain.Polygon, Blockchain.PolygonTestnet,
Blockchain.RSK,
Blockchain.Fantom, Blockchain.FantomTestnet,
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
)
removeAll { it.blockchain in unnecessaryBlockchains }
}
private fun MutableSet<BlockchainToDerive>.addSecondCardanoDerivationIfPresent() {
val cardanoDerivation = this
.firstOrNull { it.blockchain == Blockchain.Cardano }
?.derivationPath
?: return
val secondCardanoBlockchain = BlockchainToDerive(
blockchain = Blockchain.Cardano,
derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation),
)
add(secondCardanoBlockchain)
}
private fun Set<Blockchain>.mapToBlockchainsWithDerivations(
derivationStyle: DerivationStyle?,
): MutableSet<BlockchainToDerive> {
return mapTo(hashSetOf()) { blockchain ->
BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle))
}
}

View file

@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.data.wallets.derivations.MissedDerivationsFinder
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.card.common.TwinsHelper
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.configs.CardConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
@ -45,11 +43,10 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.collections.set
internal class ScanProductTask(
private val card: Card?,
private val derivationsFinder: DerivationsFinder?,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
private val visaCardScanHandler: VisaCardScanHandler?,
private val visaCoroutineScope: CoroutineScope?,
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?,
@ -81,7 +78,7 @@ internal class ScanProductTask(
readVisaCard(
session = session,
cardDto = cardDto,
scanWalletProcessor = ScanWalletProcessor(derivationsFinder),
scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder),
callback = callback,
)
return
@ -89,7 +86,7 @@ internal class ScanProductTask(
val commandProcessor = when {
cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(derivationsFinder)
else -> ScanWalletProcessor(blockchainToDeriveFinder)
}
commandProcessor.proceed(cardDto, session) { processorResult ->
when (processorResult) {
@ -170,7 +167,7 @@ internal class ScanProductTask(
}
private class ScanWalletProcessor(
private val derivationsFinder: DerivationsFinder?,
private val blockchainToDeriveFinder: BlockchainToDeriveFinder?,
) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null
@ -293,7 +290,6 @@ private class ScanWalletProcessor(
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val productType = getWalletProductType(card)
val config = CardConfig.createConfig(card)
scope.launch {
val scanResponse = ScanResponse(
card = card,
@ -301,8 +297,7 @@ private class ScanWalletProcessor(
walletData = session.environment.walletData,
primaryCard = primaryCard,
)
val derivations =
collectDerivations(card, config, scanResponse.derivationStyleProvider)
val derivations = collectDerivations(card, scanResponse)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(CompletionResult.Success(scanResponse))
return@launch
@ -332,32 +327,13 @@ private class ScanWalletProcessor(
private suspend fun collectDerivations(
card: CardDTO,
config: CardConfig,
derivationStyleProvider: DerivationStyleProvider,
scanResponse: ScanResponse,
): Map<ByteArrayKey, List<DerivationPath>> {
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
val blockchains = derivationsFinder
?.findBlockchainsToDerive(card, derivationStyleProvider)
?: return derivations
val blockchains = blockchainToDeriveFinder
?.find(card)
?: return emptyMap()
blockchains.forEach { blockchain ->
val curve = config.primaryCurve(blockchain.blockchain)
val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
if (wallet.chainCode == null) return@forEach
val key = wallet.publicKey.toMapKey()
val path = blockchain.derivationPath
if (path != null) {
val addedDerivations = derivations[key]
if (addedDerivations != null) {
derivations[key] = addedDerivations + path
} else {
derivations[key] = listOf(path)
}
}
}
return derivations
return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains)
}
}

View file

@ -4,5 +4,9 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.tokens.TokensFeatureToggles
internal class DefaultTokensFeatureToggles(
@Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isMultiAddressUtxoEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED")
}

View file

@ -28,7 +28,7 @@ class FinalizeTwinTask(
is CompletionResult.Success ->
ScanProductTask(
card = readResult.data,
derivationsFinder = null,
blockchainToDeriveFinder = null,
visaCardScanHandler = null,
visaCoroutineScope = null,
shouldCheckIsAlreadyActivated = false,

View file

@ -88,7 +88,7 @@ internal class DefaultUserWalletsListRepository(
.map { wallets.updateWith(it) }
}
.doOnSuccess { loadedWallets ->
userWallets.update { toUpdate ->
userWallets.update { _ ->
val selectedUserWalletId = selectedUserWalletRepository.get()
selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId }
?: loadedWallets.firstOrNull()?.also {
@ -240,7 +240,7 @@ internal class DefaultUserWalletsListRepository(
}
}
@Suppress("CyclomaticComplexMethod")
@Suppress("CyclomaticComplexMethod", "LongMethod")
override suspend fun unlock(
userWalletId: UserWalletId,
unlockMethod: UserWalletsListRepository.UnlockMethod,
@ -317,7 +317,13 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo ->
updateWallets { it?.updateWith(sensitiveInfo) }
updateWallets { wallets ->
// It is necessary to update derivations because when scanning we obtain the missing keys
wallets?.updateWith(
walletIdToSensitiveInformation = sensitiveInfo,
walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys),
)
}
trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card)
}
.doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) }

View file

@ -1,8 +1,10 @@
package com.tangem.tap.domain.userWalletList.utils
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
@ -72,7 +74,10 @@ internal fun List<UserWalletPublicInformation>.toUserWallets(): List<UserWallet>
return this.map { it.toUserWallet() }
}
internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet {
internal fun UserWallet.updateWith(
sensitiveInformation: UserWalletSensitiveInformation,
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap>?,
): UserWallet {
return when (this) {
is UserWallet.Cold -> {
copy(
@ -80,6 +85,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
card = scanResponse.card.copy(
wallets = requireNotNull(sensitiveInformation.wallets),
),
derivedKeys = derivedKeys ?: scanResponse.derivedKeys,
// visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
@ -92,14 +98,20 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
internal fun List<UserWallet>.updateWith(
walletIdToSensitiveInformation: Map<UserWalletId, UserWalletSensitiveInformation>,
walletIdToDerivedKeys: Map<UserWalletId, Map<KeyWalletPublicKey, ExtendedPublicKeysMap>>? = null,
): List<UserWallet> {
return if (walletIdToSensitiveInformation.isEmpty()) {
this
} else {
this.map { wallet ->
walletIdToSensitiveInformation[wallet.walletId]
?.let(wallet::updateWith)
?: wallet
val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId]
val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId)
if (sensitiveInformation != null) {
wallet.updateWith(sensitiveInformation, derivedKeys)
} else {
wallet
}
}
}
}

View file

@ -222,7 +222,7 @@ internal class CardSettingsModel @Inject constructor(
val card = scanResponse.card
modelScope.launch {
val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true
val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true
store.dispatchNavigationAction {
push(
route = AppRoute.ResetToFactory(

View file

@ -1,66 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import org.rekotlin.Middleware
@Deprecated("Will be removed soon")
object TradeCryptoMiddleware {
val middleware: Middleware<AppState> = { _, appState ->
{ nextDispatch ->
{ action ->
if (action is TradeCryptoAction) {
handle(appState, action)
}
nextDispatch(action)
}
}
}
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state)) return
when (action) {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Sell -> proceedSellAction(action)
}
}
private fun proceedSellAction(action: TradeCryptoAction.Sell) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
?: return
val currency = action.cryptoCurrencyStatus.currency
store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl(
cryptoCurrency = currency,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)?.let { url ->
store.dispatchOpenUrl(url)
Analytics.send(Token.Withdraw.ScreenOpened())
}
}
private fun openReceiptUrl(transactionId: String) {
store.dispatchNavigationAction(AppRouter::pop)
val sellService = store.inject(DaggerGraphState::appStateHolder).sellService
sellService?.getSellCryptoReceiptUrl(transactionId = transactionId)
?.let(store::dispatchOpenUrl)
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.network.auth
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.utils.Provider
@ -11,7 +11,7 @@ import com.tangem.utils.ProviderSuspend
internal class DefaultAuthProvider(
private val userWalletsListRepository: UserWalletsListRepository,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
) : AuthProvider {
override suspend fun getCardPublicKey(): String {
@ -47,11 +47,11 @@ internal class DefaultAuthProvider(
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
-> environmentConfig.tangemApiKeyDev
ApiEnvironment.STAGE_2,
ApiEnvironment.STAGE,
-> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
-> environmentConfig.tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfig.tangemApiKey
} ?: error("No tangem tech api config provided")
}
}
@ -60,8 +60,8 @@ internal class DefaultAuthProvider(
return ProviderSuspend {
when (apiEnvironment.invoke()) {
ApiEnvironment.DEV,
-> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey
-> environmentConfig.gaslessTxApiKeyDev
ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey
else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}")
} ?: error("No gasless tx api config provided")
}

View file

@ -1,15 +1,15 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
) : P2PEthPoolAuthProvider {
override fun getApiKey(): String {
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
val keys = environmentConfig.p2pApiKey
?: error("No P2P api keys provided")
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet

View file

@ -1,13 +1,13 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.lib.auth.StakeKitAuthProvider
internal class DefaultStakeKitAuthProvider(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
) : StakeKitAuthProvider {
override fun getApiKey(): String {
return environmentConfigStorage.getConfigSync().stakeKitApiKey ?: error("No StakeKit api key provided")
return environmentConfig.stakeKitApiKey ?: error("No StakeKit api key provided")
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
@ -22,11 +22,11 @@ internal class AuthModule {
@Singleton
fun provideAuthProvider(
userWalletsListRepository: UserWalletsListRepository,
environmentConfigStorage: EnvironmentConfigStorage,
environmentConfig: EnvironmentConfig,
): AuthProvider {
return DefaultAuthProvider(
userWalletsListRepository = userWalletsListRepository,
environmentConfigStorage = environmentConfigStorage,
environmentConfig = environmentConfig,
)
}
@ -38,14 +38,14 @@ internal class AuthModule {
@Provides
@Singleton
fun provideStakeKitAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): StakeKitAuthProvider {
return DefaultStakeKitAuthProvider(environmentConfigStorage)
fun provideStakeKitAuthProvider(environmentConfig: EnvironmentConfig): StakeKitAuthProvider {
return DefaultStakeKitAuthProvider(environmentConfig)
}
@Provides
@Singleton
fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider {
return DefaultP2PEthPoolAuthProvider(environmentConfigStorage)
fun provideP2PEthPoolAuthProvider(environmentConfig: EnvironmentConfig): P2PEthPoolAuthProvider {
return DefaultP2PEthPoolAuthProvider(environmentConfig)
}
@Provides

View file

@ -21,6 +21,4 @@ interface SellService {
walletAddress: String,
isDarkTheme: Boolean,
): String?
fun getSellCryptoReceiptUrl(transactionId: String): String?
}

View file

@ -19,7 +19,6 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@ -28,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec
class MoonPayService(
private val api: MoonPayApi,
private val apiKeyProvider: Provider<String>,
private val secretKeyProvider: Provider<String>,
private val apiKey: String,
private val secretKey: String,
private val userWalletProvider: () -> UserWallet?,
) : SellService {
@ -47,18 +46,18 @@ class MoonPayService(
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> {
Timber.e("Failed to load user status", result.error)
Timber.e(result.error, "Failed to load user status")
_initializationStatus.value = result.error.lceError()
return@performRequest
}
is Result.Success -> result.data
}
val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) {
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> {
Timber.e("Failed to load currencies", result.error)
Timber.e(result.error, "Failed to load currencies")
_initializationStatus.value = result.error.lceError()
return@performRequest
}
@ -163,7 +162,7 @@ class MoonPayService(
val uri = Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendQueryParameter("apiKey", apiKeyProvider())
.appendQueryParameter("apiKey", apiKey)
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
.appendQueryParameter("refundWalletAddress", walletAddress)
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
@ -177,17 +176,9 @@ class MoonPayService(
return uri.build().toString()
}
override fun getSellCryptoReceiptUrl(transactionId: String): String {
return Uri.Builder()
.scheme(SCHEME)
.authority(URL_SELL)
.appendPath("transaction_receipt")
.appendQueryParameter("transactionId", transactionId).build().toString()
}
private fun createSignature(data: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256")
val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)

View file

@ -164,4 +164,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
ArbitrumNova -> null
Plasma, PlasmaTestnet -> null
Monad, MonadTestnet -> null
Berachain -> MoonPaySupportedCurrency(networkCode = "berachain", currencyCode = "bera_bera")
BerachainTestnet -> null
}

View file

@ -11,7 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.data.card.TransactionSignerFactory
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
@ -63,7 +62,6 @@ data class DaggerGraphState(
val shareManager: ShareManager? = null,
val appRouter: AppRouter? = null,
val transactionSignerFactory: TransactionSignerFactory? = null,
val environmentConfigStorage: EnvironmentConfigStorage? = null,
val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null,
val onboardingRepository: OnboardingRepository? = null,
val excludedBlockchains: ExcludedBlockchains? = null,

View file

@ -2,7 +2,6 @@ package com.tangem.tap.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.api.ReferralComponent
@ -18,7 +17,6 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.*
import com.tangem.features.kyc.KycComponent
@ -26,7 +24,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
@ -67,7 +64,6 @@ internal class ChildFactory @Inject constructor(
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
private val onrampComponentFactory: OnrampComponent.Factory,
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
@ -117,7 +113,6 @@ internal class ChildFactory @Inject constructor(
private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val feedFeatureToggle: FeedFeatureToggle,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -147,11 +142,7 @@ internal class ChildFactory @Inject constructor(
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
}
val mode = when (val portfolio = route.portfolioId) {
is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId)
is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId)
null -> ManageTokensMode.None
}
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None
createComponentChild(
context = context,
@ -193,39 +184,21 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.MarketsTokenDetails -> {
if (feedFeatureToggle.isFeedEnabled) {
createComponentChild(
context = context,
params = FeedEntryRoute.MarketTokenDetails(
token = route.token,
appCurrency = route.appCurrency,
shouldShowPortfolio = route.shouldShowPortfolio,
analyticsParams = route.analyticsParams?.let { params ->
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
blockchain = params.blockchain,
source = params.source,
)
},
),
componentFactory = feedEntryComponentFactory,
)
} else {
createComponentChild(
context = context,
params = MarketsTokenDetailsComponent.Params(
token = route.token,
appCurrency = route.appCurrency,
shouldShowPortfolio = route.shouldShowPortfolio,
analyticsParams = route.analyticsParams?.let { params ->
MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = params.blockchain,
source = params.source,
)
},
),
componentFactory = marketsTokenDetailsComponentFactory,
)
}
createComponentChild(
context = context,
params = FeedEntryRoute.MarketTokenDetails(
token = route.token,
appCurrency = route.appCurrency,
shouldShowPortfolio = route.shouldShowPortfolio,
analyticsParams = route.analyticsParams?.let { params ->
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
blockchain = params.blockchain,
source = params.source,
)
},
),
componentFactory = feedEntryComponentFactory,
)
}
is AppRoute.Onramp -> {
createComponentChild(

View file

@ -7,7 +7,6 @@ import com.tangem.common.routing.DeepLinkScheme
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
@ -53,7 +52,6 @@ internal class DeepLinkFactory @Inject constructor(
private val promoDeepLink: PromoDeeplinkHandler.Factory,
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
private val feedFeatureToggle: FeedFeatureToggle,
) {
private val permittedAppRoute = MutableStateFlow(false)
@ -127,7 +125,7 @@ internal class DeepLinkFactory @Inject constructor(
onboardVisaDeepLink.create(deeplinkUri)
return
}
deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> {
deeplinkUri.path?.startsWith("/news") == true -> {
newsDetailsDeepLink.create(coroutineScope, deeplinkUri)
return
}

View file

@ -1,21 +1,4 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="146"
android:viewportHeight="146">
<group
android:scaleX="0.33"
android:scaleY="0.33"
android:translateX="48.91"
android:translateY="48.91">
<path
android:fillColor="#1E1E1E"
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_launcher_foreground_base" />
</layer-list>

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="146"
android:viewportHeight="146">
<group
android:scaleX="0.33"
android:scaleY="0.33"
android:translateX="48.91"
android:translateY="48.91">
<path
android:fillColor="#1E1E1E"
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
</group>
</vector>

View file

@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground_base"/>
</adaptive-icon>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="146"
android:viewportHeight="146">
<!-- Tangem logo -->
<group
android:scaleX="0.33"
android:scaleY="0.33"
android:translateX="48.91"
android:translateY="48.91">
<path
android:fillColor="#1E1E1E"
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
</group>
<!-- Colored triangle in bottom-right corner -->
<path
android:fillColor="#9C27B0"
android:pathData="M146,20 L146,146 L20,146 Z" />
<!-- MOCKED text (rotated 45 degrees) -->
<group
android:rotation="-45"
android:pivotX="91.0"
android:pivotY="91.0">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M61.2,94.6 L61.2,84.7 L64.2,84.7 L66.0,91.4 L67.8,84.7 L70.8,84.7 L70.8,94.6 L68.9,94.6 L68.9,86.8 L66.9,94.6 L65.0,94.6 L63.1,86.8 L63.1,94.6 Z M72.4,89.7 Q72.4,88.2,72.8,87.1 Q73.2,86.4,73.7,85.8 Q74.3,85.2,75.0,84.9 Q75.9,84.5,77.2,84.5 Q79.3,84.5,80.7,85.9 Q82.0,87.2,82.0,89.6 Q82.0,92.0,80.7,93.4 Q79.4,94.8,77.2,94.8 Q75.0,94.8,73.7,93.4 Q72.4,92.1,72.4,89.7 Z M74.4,89.6 Q74.4,91.3,75.2,92.2 Q76.0,93.0,77.2,93.0 Q78.4,93.0,79.1,92.2 Q79.9,91.3,79.9,89.6 Q79.9,87.9,79.2,87.0 Q78.4,86.2,77.2,86.2 Q75.9,86.2,75.2,87.1 Q74.4,87.9,74.4,89.6 Z M89.9,90.9 L91.8,91.6 Q91.4,93.2,90.3,94.0 Q89.3,94.8,87.7,94.8 Q85.7,94.8,84.5,93.4 Q83.2,92.1,83.2,89.7 Q83.2,87.2,84.5,85.9 Q85.7,84.5,87.8,84.5 Q89.6,84.5,90.8,85.6 Q91.5,86.2,91.8,87.4 L89.8,87.9 Q89.6,87.1,89.1,86.7 Q88.5,86.2,87.7,86.2 Q86.6,86.2,85.9,87.0 Q85.3,87.8,85.3,89.6 Q85.3,91.4,85.9,92.2 Q86.6,93.0,87.7,93.0 Q88.5,93.0,89.1,92.5 Q89.6,92.0,89.9,90.9 Z M93.6,94.6 L93.6,84.7 L95.6,84.7 L95.6,89.1 L99.6,84.7 L102.3,84.7 L98.6,88.5 L102.5,94.6 L99.9,94.6 L97.2,89.9 L95.6,91.6 L95.6,94.6 Z M103.5,94.6 L103.5,84.7 L110.9,84.7 L110.9,86.3 L105.5,86.3 L105.5,88.5 L110.5,88.5 L110.5,90.2 L105.5,90.2 L105.5,92.9 L111.1,92.9 L111.1,94.6 Z M112.8,84.7 L116.4,84.7 Q117.7,84.7,118.3,84.9 Q119.2,85.1,119.8,85.8 Q120.4,86.4,120.8,87.4 Q121.1,88.3,121.1,89.7 Q121.1,90.9,120.8,91.8 Q120.4,92.9,119.7,93.6 Q119.2,94.1,118.3,94.4 Q117.7,94.6,116.5,94.6 L112.8,94.6 Z M114.8,86.3 L114.8,92.9 L116.3,92.9 Q117.1,92.9,117.5,92.8 Q118.0,92.7,118.3,92.4 Q118.6,92.1,118.8,91.4 Q119.0,90.8,119.0,89.6 Q119.0,88.5,118.8,87.9 Q118.6,87.3,118.3,86.9 Q117.9,86.6,117.3,86.4 Q116.9,86.3,115.7,86.3 Z" />
</group>
</vector>

View file

@ -0,0 +1,134 @@
package com.tangem.tap.data
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.network.exchangeServices.SellService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class DefaultOfframpRepositoryTest {
private val sellService: SellService = mockk()
private val repository = DefaultOfframpRepository(sellService)
private val cryptoCurrency: CryptoCurrency = mockk()
private val fiatCurrencyCode = "USD"
private val walletAddress = "0x1234567890abcdef"
@BeforeEach
fun setUp() {
mockkObject(MutableAppThemeModeHolder)
}
@AfterEach
fun tearDown() {
clearMocks(sellService)
unmockkObject(MutableAppThemeModeHolder)
}
@Test
fun `getOfframpUrl should return url when sellService returns url with light theme`() {
// Arrange
val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light"
every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
every {
sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = false,
)
} returns expectedUrl
// Act
val result = repository.getOfframpUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyCode = fiatCurrencyCode,
walletAddress = walletAddress,
)
// Assert
assertThat(result).isEqualTo(expectedUrl)
verify(exactly = 1) {
sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = false,
)
}
}
@Test
fun `getOfframpUrl should return url when sellService returns url with dark theme`() {
// Arrange
val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark"
every { MutableAppThemeModeHolder.isDarkThemeActive } returns true
every {
sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = true,
)
} returns expectedUrl
// Act
val result = repository.getOfframpUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyCode = fiatCurrencyCode,
walletAddress = walletAddress,
)
// Assert
assertThat(result).isEqualTo(expectedUrl)
verify(exactly = 1) {
sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = true,
)
}
}
@Test
fun `getOfframpUrl should return null when sellService returns null`() {
// Arrange
every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
every {
sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = false,
)
} returns null
// Act
val result = repository.getOfframpUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyCode = fiatCurrencyCode,
walletAddress = walletAddress,
)
// Assert
assertThat(result).isNull()
verify(exactly = 1) {
sellService.getUrl(
cryptoCurrency = cryptoCurrency,
fiatCurrencyName = fiatCurrencyCode,
walletAddress = walletAddress,
isDarkTheme = false,
)
}
}
}

View file

@ -0,0 +1,252 @@
package com.tangem.tap.domain.tasks.product
import com.google.common.truth.Truth
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.wallets.derivations.BlockchainToDerive
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.wallet.UserWalletId
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
/**
[REDACTED_AUTHOR]
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class BlockchainToDeriveFinderTest {
private val walletAccountsFetcher = mockk<WalletAccountsFetcher>()
private val finder = BlockchainToDeriveFinder(
walletAccountsFetcher = walletAccountsFetcher,
)
@AfterEach
fun tearDown() {
clearMocks(walletAccountsFetcher)
}
@Test
fun `GIVEN card is not HD wallet THEN return empty set`() = runTest {
// Arrange
val card = mockk<CardDTO> {
every { this@mockk.settings.isHDWalletAllowed } returns false
}
// Act
val actual = finder.find(card)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `GIVEN card has empty wallets THEN return empty set`() = runTest {
// Arrange
val card = mockk<CardDTO> {
every { this@mockk.settings.isHDWalletAllowed } returns true
every { this@mockk.wallets } returns emptyList()
}
// Act
val actual = finder.find(card)
// Assert
Truth.assertThat(actual).isEmpty()
}
@Test
fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest {
// Arrange
val card = createCardDTO()
val response = createResponse(Blockchain.Bitcoin)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest {
// Arrange
val demoCardId = "AC01000000045754"
val card = createCardDTO(cardId = demoCardId)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
createExpected(Blockchain.Dogecoin),
createExpected(Blockchain.Solana),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest {
// Arrange
val demoCardId = "DE00"
val card = createCardDTO(cardId = demoCardId)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
createExpected(Blockchain.Dogecoin),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN empty store THEN return default blockchains`() = runTest {
// Arrange
val card = createCardDTO()
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Bitcoin),
createExpected(Blockchain.Ethereum),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN saved cardano THEN return only cardano`() = runTest {
// Arrange
val card = createCardDTO()
val response = createResponse(Blockchain.Cardano)
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act
val actual = finder.find(card)
// Assert
val expected = setOf(
createExpected(Blockchain.Cardano),
)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
@Test
fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest {
// Arrange
val card = createCardDTO()
val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon)
val response = createResponse(*blockchains.toTypedArray())
coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response
// Act
val actual = finder.find(card)
// Assert
val expected = blockchains.mapTo(hashSetOf(), ::createExpected)
Truth.assertThat(actual).containsExactlyElementsIn(expected)
coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) }
}
private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO {
val wallet = mockk<CardDTO.Wallet> {
every { this@mockk.publicKey } returns byteArrayOf(0)
}
return mockk<CardDTO> {
every { this@mockk.cardId } returns cardId
every { this@mockk.batchId } returns batchId
every { this@mockk.settings.isHDWalletAllowed } returns true
every { this@mockk.settings.isKeysImportAllowed } returns true
every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion(
major = 6,
minor = 33,
patch = 0,
type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release,
)
every { this@mockk.wallets } returns listOf(wallet)
}
}
private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse {
val tokens = blockchains.map { blockchain ->
mockk<UserTokensResponse.Token> {
every { this@mockk.networkId } returns blockchain.toNetworkId()
every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath
every { this@mockk.contractAddress } returns null
}
}
val account = mockk<WalletAccountDTO> {
every { this@mockk.tokens } returns tokens
}
return mockk {
every { this@mockk.accounts } returns listOf(account)
}
}
private fun createExpected(
blockchain: Blockchain,
derivationPath: DerivationPath = blockchain.getDerivationPath(),
): BlockchainToDerive {
return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath)
}
private fun Blockchain.getDerivationPath(): DerivationPath {
return derivationPath(DerivationStyle.V3)!!
}
private companion object {
// for byteArrayOf(0)
val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7")
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.common.routing.AppRoute
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler
import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler
import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler
import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler
@ -87,7 +86,6 @@ class DeepLinkFactoryTest {
private val newsDeeplink = mockk<NewsDetailsDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val feedFeatureToggle = mockk<FeedFeatureToggle>()
private val mockedUri = mockk<Uri>(relaxed = true)
private val isFromOnNewIntent: Boolean = false
@ -112,7 +110,6 @@ class DeepLinkFactoryTest {
promoDeepLink = promoDeepLinkFactory,
onboardVisaDeepLink = onboardVisaDeepLink,
newsDetailsDeepLink = newsDeeplink,
feedFeatureToggle = feedFeatureToggle,
)
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -1,6 +1,3 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import java.util.concurrent.ConcurrentHashMap
plugins {
alias(deps.plugins.kotlin.android) apply false
alias(deps.plugins.kotlin.jvm) apply false
@ -33,83 +30,13 @@ interface Injected {
val fs: FileSystemOperations
}
data class TestStats(
val total: Long = 0,
val passed: Long = 0,
val failed: Long = 0,
val skipped: Long = 0,
)
val testResultsByModule = ConcurrentHashMap<String, TestStats>()
// Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules
val unitTest by tasks.registering {
group = "verification"
description = "Run unit tests for debug/googleDebug variant and all JVM modules"
doLast {
if (testResultsByModule.isNotEmpty()) {
val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats ->
TestStats(
total = acc.total + stats.total,
passed = acc.passed + stats.passed,
failed = acc.failed + stats.failed,
skipped = acc.skipped + stats.skipped,
)
}
println("\n" + "=".repeat(80))
println("TEST SUMMARY")
println("=".repeat(80))
testResultsByModule.toSortedMap().forEach { (module, stats) ->
println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)")
}
println("-".repeat(80))
println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules")
println(" Passed: ${totalStats.passed}")
println(" Failed: ${totalStats.failed}")
println(" Skipped: ${totalStats.skipped}")
println("=".repeat(80))
}
}
}
// Test Logging and testCI dependencies
subprojects {
tasks.withType<Test>().configureEach {
println("Test task scheduled: $path")
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showStandardStreams = true
afterSuite(KotlinClosure2<TestDescriptor, TestResult, Unit>({ desc, result ->
if (desc.parent == null) { // will match the outermost suite
testResultsByModule[path] = TestStats(
total = result.testCount,
passed = result.successfulTestCount,
failed = result.failedTestCount,
skipped = result.skippedTestCount,
)
val output =
"Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
val startItem = "| "
val endItem = " |"
val repeatLength = startItem.length + output.length + endItem.length
println(
"\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat(
repeatLength
)
)
}
}))
}
}
// Register testCI dependencies
// App module
plugins.withId("com.android.application") {
afterEvaluate {

View file

@ -7,13 +7,12 @@ import android.os.Bundle
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.serialization.SerializedBigDecimal
@ -21,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.pay.TangemPayDetailsConfig
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.tokens.model.details.NavigationAction
import kotlinx.serialization.Serializable
@ -128,8 +128,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class ManageTokens(
val source: Source,
val portfolioId: PortfolioId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
val accountId: AccountId? = null,
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${accountId?.value}") {
/**
* Source of launching the screen.
@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class EditAccount(
val account: Account,
val account: Account.CryptoPortfolio,
) : AppRoute(path = "/edit_account/${account.accountId.value}")
@Serializable
data class AccountDetails(
val account: Account,
val account: Account.CryptoPortfolio,
) : AppRoute(path = "/account_details/${account.accountId.value}")
@Serializable

View file

@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine
object TangemSiteUrlBuilder {
const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10"
suspend fun getUtmTags(campaign: String?): String {
val langCode = Locale.getDefault().language
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()

View file

@ -73,7 +73,7 @@ object MockScanResponseFactory {
CardDTO.Wallet(
CardWallet(
publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name
chainCode = null,
chainCode = ByteArray(32), // chainCode must not be null for HD wallets
curve = curve,
settings = createSettings(),
totalSignedHashes = null,

View file

@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter(
)
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(this),
iconState = AccountIconItemStateConverter().convert(this),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content {
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(account),
iconState = AccountIconItemStateConverter().convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable {
return TokenItemState.Unreachable(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(account),
iconState = AccountIconItemStateConverter().convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),

View file

@ -1,11 +1,14 @@
package com.tangem.common.ui.account
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.converter.Converter
object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.CryptoPortfolio> {
class AccountIconItemStateConverter(
val size: AccountIconSize = AccountIconSize.Default,
) : Converter<Account, CurrencyIconState.CryptoPortfolio> {
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
is Account.CryptoPortfolio -> when {
@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.Cryp
char = value.accountName.toUM().value,
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
else -> CurrencyIconState.CryptoPortfolio.Icon(
resId = value.icon.value.getResId(),
color = value.icon.color.getUiColor(),
isGrayscale = false,
size = size,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")

View file

@ -0,0 +1,31 @@
package com.tangem.common.ui.account
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
class TokensListPortfolioItemConverter(
val tokenItemUM: TokenItemState,
val isExpanded: Boolean,
val isCollapsable: Boolean,
val tokens: ImmutableList<PortfolioTokensListItemUM>,
val onEmptyAction: PortfolioItemContentUM.Empty.Action? = null,
) : Converter<Unit, TokensListItemUM.Portfolio> {
override fun convert(value: Unit): TokensListItemUM.Portfolio {
val content = if (tokens.isEmpty()) {
PortfolioItemContentUM.Empty(onEmptyAction)
} else {
PortfolioItemContentUM.Tokens(tokens)
}
return TokensListItemUM.Portfolio(
tokenItemUM = tokenItemUM,
isExpanded = isExpanded,
isCollapsable = isCollapsable,
content = content,
)
}
}

View file

@ -1,56 +0,0 @@
package com.tangem.common.ui.alerts
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.utils.converter.Converter
class TransactionErrorAlertConverter(
private val popBackStack: () -> Unit,
private val onFailedTxEmailClick: (String) -> Unit,
) : Converter<SendTransactionError, AlertUM?> {
override fun convert(value: SendTransactionError): AlertUM? {
return when (value) {
is SendTransactionError.DemoCardError -> AlertDemoModeUM(
onConfirmClick = popBackStack,
)
is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = null,
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = value.message,
onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
)
is SendTransactionError.DataError -> AlertTransactionErrorUM(
code = "",
cause = value.message,
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
code = value.code.orEmpty(),
cause = value.message.orEmpty(),
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
code = "",
cause = value.ex?.localizedMessage,
onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
)
is SendTransactionError.CreateAccountUnderfunded -> AlertTransactionErrorUM(
code = "",
cause = null,
causeTextReference = resourceReference(R.string.no_account_polkadot, wrappedList(value.amount)),
onConfirmClick = popBackStack,
)
else -> null
}
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.common.ui.alerts
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.transaction.error.SendTransactionError
import javax.inject.Inject
class TransactionErrorDialogFactory @Inject constructor() {
fun create(
error: SendTransactionError,
popBackStack: () -> Unit,
onFailedTxEmailClick: (String) -> Unit,
): DialogMessage? {
return when (error) {
is SendTransactionError.DemoCardError -> demoModeDialog(popBackStack)
is SendTransactionError.TangemSdkError -> transactionErrorDialog(
causeTextReference = resourceReference(error.messageRes, wrappedList(error.args)),
code = error.code.toString(),
onConfirmClick = { onFailedTxEmailClick(error.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> transactionErrorDialog(
cause = error.message,
code = error.code.toString(),
onConfirmClick = { onFailedTxEmailClick("${error.code}: ${error.message.orEmpty()}") },
)
is SendTransactionError.DataError -> transactionErrorDialog(
cause = error.message,
code = "",
onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) },
)
is SendTransactionError.NetworkError -> transactionErrorDialog(
cause = error.message.orEmpty(),
code = error.code.orEmpty(),
onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) },
)
is SendTransactionError.UnknownError -> transactionErrorDialog(
cause = error.ex?.localizedMessage,
code = "",
onConfirmClick = { onFailedTxEmailClick(error.ex?.localizedMessage.orEmpty()) },
)
is SendTransactionError.CreateAccountUnderfunded -> transactionErrorDialog(
causeTextReference = resourceReference(
R.string.no_account_polkadot,
wrappedList(error.amount),
),
code = "",
onConfirmClick = popBackStack,
)
else -> null
}
}
private fun demoModeDialog(onConfirmClick: () -> Unit): DialogMessage = DialogMessage(
title = resourceReference(id = R.string.warning_demo_mode_title),
message = resourceReference(id = R.string.warning_demo_mode_message),
firstAction = EventMessageAction(
title = resourceReference(id = R.string.common_ok),
onClick = onConfirmClick,
),
)
private fun transactionErrorDialog(
cause: String? = null,
causeTextReference: TextReference? = null,
code: String,
onConfirmClick: () -> Unit,
): DialogMessage = DialogMessage(
title = resourceReference(id = R.string.send_alert_transaction_failed_title),
message = resourceReference(
id = R.string.send_alert_transaction_failed_text,
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
),
firstAction = EventMessageAction(
title = resourceReference(id = R.string.common_support),
onClick = onConfirmClick,
),
)
}

View file

@ -1,13 +0,0 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
data class AlertDemoModeUM(
override val onConfirmClick: () -> Unit,
) : AlertUM {
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
}

View file

@ -1,21 +0,0 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
data class AlertTransactionErrorUM(
val code: String,
val cause: String?,
val causeTextReference: TextReference? = null,
override val onConfirmClick: () -> Unit,
) : AlertUM {
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
override val message: TextReference = resourceReference(
id = R.string.send_alert_transaction_failed_text,
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}

View file

@ -1,12 +0,0 @@
package com.tangem.common.ui.alerts.models
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
interface AlertUM {
val title: TextReference?
val message: TextReference
val confirmButtonText: TextReference
val onConfirmClick: (() -> Unit)?
}

View file

@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@Deprecated("Use GiveApprovalComponent")
@Composable
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
var isPermissionAlertShow by remember { mutableStateOf(false) }

View file

@ -1,64 +0,0 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
SpacerH(12.dp)
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
SpacerH(14.dp)
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
SpacerH(32.dp)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
}
}
}
}
@Composable
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(modifier = Modifier.padding(12.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
SpacerH(12.dp)
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
SpacerH(6.dp)
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
SpacerH(16.dp)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
}
}
}
}

View file

@ -1,17 +1,35 @@
package com.tangem.common.ui.notifications
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.ds.TangemPagerIndicator
import com.tangem.core.ui.ds.message.TangemMessage
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
fun LazyListScope.notifications(
notifications: ImmutableList<NotificationUM>,
@ -106,8 +124,107 @@ fun LazyListScope.notifications(
contentColor = contentColor,
modifier = modifier
.padding(top = topPadding)
.animateItem(),
.animateItem(null, null, null),
)
},
)
}
}
/**
* Displays a list of notifications in a stacked manner using a HorizontalPager.
* If there are multiple notifications, a PagerIndicator is shown below the notifications.
*
* @param notifications List of TangemMessageUM objects to be displayed.
* @param containerColor Color to be used for the background of the notifications.
* @param modifier Optional Modifier for the notifications.
*/
fun LazyListScope.notificationsCarousel(
notifications: ImmutableList<TangemMessageUM>?,
containerColor: Color,
modifier: Modifier = Modifier,
) {
item {
if (!notifications.isNullOrEmpty()) {
val notificationsPagerState = rememberPagerState(
pageCount = { notifications.size },
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = TangemTheme.dimens2.x2),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
HorizontalPager(
state = notificationsPagerState,
modifier = Modifier
.fillMaxSize()
.animateItem(null, null, null),
) { page ->
TangemMessage(
messageUM = notifications[page],
contentColor = containerColor,
modifier = modifier,
)
}
if (notifications.size > 1) {
TangemPagerIndicator(
pagerState = notificationsPagerState,
)
}
}
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun StackedNotifications_Preview(
@PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList<TangemMessageUM>,
) {
TangemThemePreviewRedesign {
val contentColor = TangemTheme.colors2.surface.level1
LazyColumn(
modifier = Modifier
.background(contentColor)
.padding(16.dp),
) {
notificationsCarousel(
notifications = params,
containerColor = contentColor,
)
}
}
}
private class StackedNotificationsPreviewProvider : PreviewParameterProvider<ImmutableList<TangemMessageUM>> {
override val values: Sequence<ImmutableList<TangemMessageUM>>
get() = sequenceOf(
persistentListOf(
TangemMessageUM(
id = "0",
title = stringReference("First notification"),
subtitle = stringReference("This is the first notification"),
messageEffect = TangemMessageEffect.Magic,
),
),
persistentListOf(
TangemMessageUM(
id = "0",
title = stringReference("First notification"),
subtitle = stringReference("This is the first notification"),
messageEffect = TangemMessageEffect.Magic,
),
TangemMessageUM(
id = "1",
title = stringReference("Second notification"),
subtitle = stringReference("This is the second notification"),
messageEffect = TangemMessageEffect.Card,
),
),
)
}
// endregion

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.userwallet.converter
import androidx.compose.ui.graphics.Color
import androidx.core.graphics.toColorInt
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.domain.models.wallet.UserWalletIcon
import com.tangem.utils.converter.Converter
import javax.inject.Inject
/**
* Converter for mapping [UserWalletIcon] to [DeviceIconUM],
* which is used for displaying the wallet icon in the UI.
*/
class WalletIconUMConverter @Inject constructor() : Converter<UserWalletIcon, DeviceIconUM> {
override fun convert(value: UserWalletIcon): DeviceIconUM = with(value) {
fun String.parseHexColor(): Color = try {
Color(this.toColorInt())
} catch (_: IllegalArgumentException) {
Color.Unspecified
}
return when (this) {
UserWalletIcon.Hot -> DeviceIconUM.Mobile
is UserWalletIcon.Stub ->
DeviceIconUM.Stub(cardsCount = this.cardsCount)
is UserWalletIcon.Default -> if (isRing) {
DeviceIconUM.Ring(
mainColor = Color.Unspecified,
cardColor = Color.Unspecified,
secondCardColor = if (cardsCount > 2) Color.Unspecified else null,
)
} else {
DeviceIconUM.Card(
mainColor = Color.Unspecified,
secondColor = if (cardsCount > 1) Color.Unspecified else null,
thirdColor = if (cardsCount > 2) Color.Unspecified else null,
)
}
is UserWalletIcon.Colored -> if (isRing) {
DeviceIconUM.Ring(
mainColor = mainColor.parseHexColor(),
cardColor = secondColor?.parseHexColor(),
secondCardColor = thirdColor?.parseHexColor(),
)
} else {
DeviceIconUM.Card(
mainColor = mainColor.parseHexColor(),
secondColor = secondColor?.parseHexColor(),
thirdColor = thirdColor?.parseHexColor(),
)
}
}
}
}

View file

@ -5,8 +5,7 @@ import com.tangem.core.abtests.BuildConfig
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
import com.tangem.core.abtests.manager.impl.StubABTestsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.utils.Provider
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -24,7 +23,7 @@ internal object ABTestsManagerModule {
@Singleton
fun provideABTestsManager(
application: Application,
environmentConfigStorage: EnvironmentConfigStorage,
environmentConfig: EnvironmentConfig,
dispatchers: CoroutineDispatcherProvider,
): ABTestsManager {
return if (BuildConfig.AB_TESTS_ENABLED) {
@ -32,7 +31,7 @@ internal object ABTestsManagerModule {
} else {
AmplitudeABTestsManager(
application = application,
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
apiKey = environmentConfig.amplitudeApiKey,
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}

View file

@ -7,14 +7,13 @@ import com.amplitude.experiment.ExperimentConfig
import com.amplitude.experiment.ExperimentUser
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.utils.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
internal class AmplitudeABTestsManager(
val application: Application,
val apiKeyProvider: Provider<String>,
val apiKey: String,
val scope: CoroutineScope,
) : ABTestsManager {
@ -28,7 +27,7 @@ internal class AmplitudeABTestsManager(
client = Experiment.initializeWithAmplitudeAnalytics(
application = application,
apiKey = apiKeyProvider(),
apiKey = apiKey,
config = ExperimentConfig
.builder()
.automaticFetchOnAmplitudeIdentityChange(true)

View file

@ -0,0 +1,17 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
/**
* Offramp (withdraw/sell) analytics events
*/
sealed class OfframpAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) {
/**
* Withdraw screen opened event
*/
data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened")
}

View file

@ -7,14 +7,7 @@
"name": "VISA_ONBOARDING_ENABLED",
"version": "undefined"
},
{
"name": "STAKING_TON_ENABLED",
"version": "5.28.0"
},
{
"name": "STAKING_CARDANO_ENABLED",
"version": "5.31.1"
},
{
"name": "STAKING_ETH_ENABLED",
"version": "undefined"
@ -31,38 +24,10 @@
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
"version": "5.32.0"
},
{
"name": "TANGEM_PAY_ENABLED",
"version": "5.31.0"
},
{
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
"version": "5.30.0"
},
{
"name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
"version": "5.33.0"
},
{
"name": "NEW_ONRAMP_MAIN_ENABLED",
"version": "5.31.0"
},
{
"name": "ACCOUNTS_FEATURE_ENABLED",
"version": "5.33.0"
},
{
"name": "FEED_ENABLED",
"version": "5.33.0"
},
{
"name": "APP_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "GASLESS_TRANSACTIONS_ENABLED",
"version": "5.33.0"
},
{
"name": "SWAP_MARKET_LIST_ENABLED",
"version": "5.34"
@ -78,5 +43,17 @@
{
"name": "WALLET_REORDER_FEATURE_ENABLED",
"version": "5.34"
},
{
"name": "GASLESS_APPROVAL_ENABLED",
"version": "undefined"
},
{
"name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED",
"version": "undefined"
},
{
"name": "MULTI_ADDRESS_UTXO_ENABLED",
"version": "undefined"
}
]

View file

@ -1,4 +1,6 @@
import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
import com.tangem.plugin.configuration.model.BuildType
plugins {
alias(deps.plugins.android.library)
@ -10,6 +12,23 @@ plugins {
id("configuration")
}
abstract class GenerateEnvironmentConfigTask : DefaultTask() {
@get:InputFile
abstract val configFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val input = configFile.get().asFile
require(input.exists()) { "Config file not found: ${input.absolutePath}" }
logger.lifecycle("Generating EnvironmentConfig from ${input.name}")
EnvironmentConfigGenerator.generate(input, outputDir.get().asFile)
}
}
android {
namespace = "com.tangem.datasource"
@ -18,6 +37,27 @@ android {
}
}
androidComponents {
onVariants { variant ->
val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug
val configFile = rootProject.file(
"app/src/main/assets/tangem-app-config/config_${buildType.environment}.json",
)
val taskProvider = tasks.register<GenerateEnvironmentConfigTask>(
"generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}",
) {
this.configFile.set(configFile)
outputDir.set(layout.buildDirectory.dir("generated/source/environment-config/${variant.name}"))
doFirst {
logger.lifecycle("[Environment config] Running: ${this.name}")
}
}
variant.sources.java?.addGeneratedSourceDirectory(taskProvider, GenerateEnvironmentConfigTask::outputDir)
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

View file

@ -1,11 +1,10 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
import kotlinx.coroutines.flow.first
internal class BlockAid(
private val configStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
@ -21,9 +20,7 @@ internal class BlockAid(
put(
key = "X-API-KEY",
value = ProviderSuspend {
requireNotNull(
configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey,
)
requireNotNull(environmentConfig.blockAidApiKey)
},
)
put("accept", ProviderSuspend { "application/json" })

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.utils.RequestHeader
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.ProviderSuspend
@ -11,13 +11,13 @@ import com.tangem.utils.version.AppVersionProvider
/**
* Express [ApiConfig]
*
* @property environmentConfigStorage environment config storage
* @property environmentConfig environment config
* @property expressAuthProvider express auth provider
* @property appVersionProvider app version provider
* @property appInfoProvider app info provider
*/
internal class Express(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
private val expressAuthProvider: ExpressAuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
@ -100,9 +100,9 @@ internal class Express(
private fun getApiKey(isProd: Boolean): String {
return if (isProd) {
environmentConfigStorage.getConfigSync().express
environmentConfig.express
} else {
environmentConfigStorage.getConfigSync().devExpress
environmentConfig.devExpress
}
?.apiKey
?: error("No express config provided")

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal sealed class TangemPay(
private val environmentConfig: EnvironmentConfig,
private val appVersionProvider: AppVersionProvider,
private val environmentConfigStorage: EnvironmentConfigStorage,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
@ -61,8 +61,8 @@ internal sealed class TangemPay(
return when (apiEnvironment) {
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
-> environmentConfigStorage.getConfigSync().bffStaticTokenDev
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken
-> environmentConfig.bffStaticTokenDev
ApiEnvironment.PROD -> environmentConfig.bffStaticToken
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
ApiEnvironment.DEV_2,
@ -72,9 +72,9 @@ internal sealed class TangemPay(
}
class Bff(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
environmentConfigStorage: EnvironmentConfigStorage,
) : TangemPay(appVersionProvider, environmentConfigStorage) {
) : TangemPay(environmentConfig, appVersionProvider) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
@ -90,9 +90,9 @@ internal sealed class TangemPay(
}
class Auth(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
environmentConfigStorage: EnvironmentConfigStorage,
) : TangemPay(appVersionProvider, environmentConfigStorage) {
) : TangemPay(environmentConfig, appVersionProvider) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"

View file

@ -2,7 +2,7 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
@ -10,7 +10,7 @@ import com.tangem.utils.version.AppVersionProvider
/** YieldSupply [ApiConfig] */
internal class YieldSupply(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
@ -78,8 +78,8 @@ internal class YieldSupply(
ApiEnvironment.DEV_3,
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
-> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey
-> environmentConfig.yieldModuleApiKeyDev
ApiEnvironment.PROD -> environmentConfig.yieldModuleApiKey
} ?: error("No tangem tech api config provided")
}
}

View file

@ -12,7 +12,7 @@ data class OrderResponse(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String?,
@Json(name = "type") val type: String?,
@Json(name = "status") val status: String,
@Json(name = "status") val status: Status,
@Json(name = "step") val step: String?,
@Json(name = "data") val data: Data,
@Json(name = "step_change_code") val stepChangeCode: Int?,
@ -29,5 +29,20 @@ data class OrderResponse(
@Json(name = "payment_account_id") val paymentAccountId: String?,
@Json(name = "transaction_hash") val transactionHash: String?,
)
@JsonClass(generateAdapter = false)
enum class Status {
@Json(name = "NEW")
NEW,
@Json(name = "PROCESSING")
PROCESSING,
@Json(name = "COMPLETED")
COMPLETED,
@Json(name = "CANCELED")
CANCELED,
}
}
}

View file

@ -5,10 +5,10 @@ import com.tangem.crypto.CryptoUtils
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
internal class Sha256SignatureVerifier(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val environmentConfig: EnvironmentConfig,
private val apiConfigsManager: ApiConfigsManager,
) : DataSignatureVerifier {
@ -24,8 +24,8 @@ internal class Sha256SignatureVerifier(
private fun getPubKey(): String? {
val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express)
return when (expressConfig.environment) {
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().express?.signVerifierPublicKey
else -> environmentConfigStorage.getConfigSync().devExpress?.signVerifierPublicKey
ApiEnvironment.PROD -> environmentConfig.express?.signVerifierPublicKey
else -> environmentConfig.devExpress?.signVerifierPublicKey
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.datasource.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.config.*
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
@ -21,13 +21,13 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideExpressConfig(
environmentConfigStorage: EnvironmentConfigStorage,
environmentConfig: EnvironmentConfig,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig {
return Express(
environmentConfigStorage = environmentConfigStorage,
environmentConfig = environmentConfig,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
@ -73,12 +73,12 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideYieldSupplyConfig(
environmentConfigStorage: EnvironmentConfigStorage,
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = YieldSupply(
environmentConfigStorage = environmentConfigStorage,
environmentConfig = environmentConfig,
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,
@ -87,21 +87,21 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemPayBffConfig(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
environmentConfigStorage: EnvironmentConfigStorage,
): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage)
): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider)
@Provides
@IntoSet
fun provideTangemPayAuthConfig(
environmentConfig: EnvironmentConfig,
appVersionProvider: AppVersionProvider,
environmentConfigStorage: EnvironmentConfigStorage,
): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage)
): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider)
@Provides
@IntoSet
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
return BlockAid(environmentConfigStorage)
fun provideBlockAidConfig(environmentConfig: EnvironmentConfig): ApiConfig {
return BlockAid(environmentConfig)
}
@Provides

View file

@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
import com.tangem.datasource.utils.SerializeNullsFactory
import com.tangem.domain.models.scan.serialization.*
import dagger.Module
@ -45,6 +46,15 @@ class MoshiModule {
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
)
.add(
NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java)
.withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created")
.withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status")
.withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card")
.withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked")
.withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance")
.withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"),
)
.add(
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")

View file

@ -3,7 +3,7 @@ package com.tangem.datasource.di
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.crypto.Sha256SignatureVerifier
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,9 +17,9 @@ internal object SecurityModule {
@Provides
@Singleton
fun provideDataSignatureVerifier(
environmentConfigStorage: EnvironmentConfigStorage,
environmentConfig: EnvironmentConfig,
apiConfigsManager: ApiConfigsManager,
): DataSignatureVerifier {
return Sha256SignatureVerifier(environmentConfigStorage, apiConfigsManager)
return Sha256SignatureVerifier(environmentConfig, apiConfigsManager)
}
}

View file

@ -1,9 +1,8 @@
package com.tangem.datasource.di.local.config
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.environment.converter.GeneratedEnvironmentConfigConverter
import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage
@ -23,11 +22,8 @@ internal object ConfigModule {
@Provides
@Singleton
fun provideEnvironmentConfigStorage(assetLoader: AssetLoader): EnvironmentConfigStorage {
return DefaultEnvironmentConfigStorage(
assetLoader = assetLoader,
environmentConfigStore = RuntimeStateStore(defaultValue = EnvironmentConfig()),
)
fun provideEnvironmentConfig(): EnvironmentConfig {
return GeneratedEnvironmentConfigConverter.convert()
}
@Provides

View file

@ -1,41 +0,0 @@
package com.tangem.datasource.local.config.environment
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.config.environment.converter.EnvironmentConfigConverter
import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
import com.tangem.datasource.local.datastore.RuntimeStateStore
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
/**
* Default implementation for storing [EnvironmentConfig]
*
* @property assetLoader asset loader
* @property environmentConfigStore config store
*/
internal class DefaultEnvironmentConfigStorage(
private val assetLoader: AssetLoader,
private val environmentConfigStore: RuntimeStateStore<EnvironmentConfig>,
) : EnvironmentConfigStorage {
override suspend fun initialize(): EnvironmentConfig {
val environmentConfigModel = assetLoader.load<EnvironmentConfigModel>(fileName = CONFIG_FILE_NAME)
?: return environmentConfigStore.get().value
val config = EnvironmentConfigConverter.convert(value = environmentConfigModel)
environmentConfigStore.store(value = config)
Timber.i("Config [$CONFIG_FILE_NAME] loaded successfully")
return config
}
override fun getConfig(): Flow<EnvironmentConfig> = environmentConfigStore.get()
override fun getConfigSync(): EnvironmentConfig = environmentConfigStore.get().value
private companion object {
const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}"
}
}

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