Updated on 2026-08-14
This commit is contained in:
commit
a70c0ae57f
731 changed files with 17726 additions and 23716 deletions
|
|
@ -152,6 +152,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 +305,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 +383,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)
|
||||
|
|
|
|||
|
|
@ -146,11 +146,8 @@ 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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
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? {
|
||||
return if (data.logConfig.isCustomerIoLogEnabled) {
|
||||
CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
|
||||
} else if (data.config.customerIoCdpApiKey.isNotBlank()) {
|
||||
CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(
|
||||
application = data.application,
|
||||
cdpApiKey = data.config.customerIoCdpApiKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -39,28 +40,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(
|
||||
|
|
@ -87,20 +66,6 @@ internal object TokensDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRemoveCurrencyUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): RemoveCurrencyUseCase {
|
||||
return RemoveCurrencyUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyUseCase(
|
||||
|
|
@ -176,34 +141,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(
|
||||
|
|
@ -378,6 +315,7 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletBalanceFetcher {
|
||||
|
|
@ -388,6 +326,7 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ class FinalizeTwinTask(
|
|||
is CompletionResult.Success ->
|
||||
ScanProductTask(
|
||||
card = readResult.data,
|
||||
derivationsFinder = null,
|
||||
blockchainToDeriveFinder = null,
|
||||
visaCardScanHandler = null,
|
||||
visaCoroutineScope = null,
|
||||
shouldCheckIsAlreadyActivated = false,
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,4 @@ interface SellService {
|
|||
walletAddress: String,
|
||||
isDarkTheme: Boolean,
|
||||
): String?
|
||||
|
||||
fun getSellCryptoReceiptUrl(transactionId: String): String?
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,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 +25,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 +65,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 +114,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")
|
||||
|
|
@ -193,39 +189,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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)?
|
||||
}
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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,30 +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"
|
||||
|
|
@ -78,5 +51,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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
|
@ -28,4 +28,5 @@ data class EnvironmentConfig(
|
|||
val bffStaticTokenDev: String? = null,
|
||||
val gaslessTxApiKeyDev: String? = null,
|
||||
val gaslessTxApiKey: String? = null,
|
||||
val customerIoCdpApiKey: String = "",
|
||||
)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Storage for [EnvironmentConfig]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface EnvironmentConfigStorage {
|
||||
|
||||
/** Initialize and return [EnvironmentConfig] */
|
||||
suspend fun initialize(): EnvironmentConfig
|
||||
|
||||
/** Get [EnvironmentConfig] as [Flow] */
|
||||
fun getConfig(): Flow<EnvironmentConfig>
|
||||
|
||||
/** Get [EnvironmentConfig] synchronously */
|
||||
fun getConfigSync(): EnvironmentConfig
|
||||
}
|
||||
|
|
@ -34,6 +34,14 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
|||
apiKey = value.quiknodeMonadApiKey,
|
||||
subdomain = value.quiknodeMonadSubdomain,
|
||||
),
|
||||
quickNodeBerachainCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeBerachainApiKey,
|
||||
subdomain = value.quiknodeBerachainSubdomain,
|
||||
),
|
||||
quickNodeStellarCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeStellarApiKey,
|
||||
subdomain = value.quiknodeStellarSubdomain,
|
||||
),
|
||||
infuraProjectId = value.infuraProjectId,
|
||||
tronGridApiKey = value.tronGridApiKey,
|
||||
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
||||
|
|
@ -116,6 +124,7 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
|||
tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest),
|
||||
monad = GetBlockAccessToken(rest = accessTokens.monad?.rest),
|
||||
stellar = GetBlockAccessToken(rest = accessTokens.stellar?.rest),
|
||||
berachain = GetBlockAccessToken(jsonRpc = accessTokens.berachain?.jsonRPC),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.datasource.local.config.environment.converter
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey
|
||||
import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey
|
||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||
import com.tangem.datasource.local.config.environment.models.P2PKeys
|
||||
|
||||
/**
|
||||
* Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig]
|
||||
*
|
||||
* This converter maps the auto-generated config (from JSON) to the domain model.
|
||||
* The generated config has nested objects that mirror the JSON structure.
|
||||
*/
|
||||
internal object GeneratedEnvironmentConfigConverter {
|
||||
|
||||
fun convert(): EnvironmentConfig {
|
||||
return EnvironmentConfig(
|
||||
moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey,
|
||||
moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey,
|
||||
mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId,
|
||||
mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret,
|
||||
blockchainSdkConfig = createBlockchainSdkConfig(),
|
||||
amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey,
|
||||
appsFlyerApiKey = AppsFlyer.appsFlyerDevKey,
|
||||
appsAppId = AppsFlyer.appsFlyerAppID,
|
||||
walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId,
|
||||
express = createExpressModel(
|
||||
apiKey = Express.apiKey,
|
||||
signVerifierPublicKey = Express.signVerifierPublicKey,
|
||||
),
|
||||
devExpress = createExpressModel(
|
||||
apiKey = DevExpress.apiKey,
|
||||
signVerifierPublicKey = DevExpress.signVerifierPublicKey,
|
||||
),
|
||||
stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey,
|
||||
p2pApiKey = createP2PKeys(),
|
||||
blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey,
|
||||
tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey,
|
||||
tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev,
|
||||
tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage,
|
||||
yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey,
|
||||
yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev,
|
||||
bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken,
|
||||
bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev,
|
||||
gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev,
|
||||
gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? {
|
||||
return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) {
|
||||
ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createP2PKeys(): P2PKeys? {
|
||||
val mainnet = P2pApiKey.mainnet
|
||||
val hoodi = P2pApiKey.hoodi
|
||||
return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) {
|
||||
P2PKeys(mainnet = mainnet, hoodi = hoodi)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQuickNodeCredentials(apiKey: String?, subdomain: String?): QuickNodeCredentials? {
|
||||
return if (!apiKey.isNullOrEmpty() && !subdomain.isNullOrEmpty()) {
|
||||
QuickNodeCredentials(apiKey = apiKey, subdomain = subdomain)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBlockchainSdkConfig(): BlockchainSdkConfig {
|
||||
return BlockchainSdkConfig(
|
||||
blockchairCredentials = BlockchairCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.blockchairApiKeys,
|
||||
authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken,
|
||||
),
|
||||
blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(),
|
||||
quickNodeSolanaCredentials = QuickNodeCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.quiknodeApiKey,
|
||||
subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain,
|
||||
),
|
||||
quickNodeBscCredentials = QuickNodeCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey,
|
||||
subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain,
|
||||
),
|
||||
quickNodePlasmaCredentials = QuickNodeCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey,
|
||||
subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain,
|
||||
),
|
||||
quickNodeMonadCredentials = QuickNodeCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey,
|
||||
subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain,
|
||||
),
|
||||
quickNodeBerachainCredentials = createQuickNodeCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.quiknodeBerachainApiKey,
|
||||
subdomain = GeneratedEnvironmentConfig.quiknodeBerachainSubdomain,
|
||||
),
|
||||
quickNodeStellarCredentials = QuickNodeCredentials(
|
||||
apiKey = GeneratedEnvironmentConfig.quiknodeStellarApiKey,
|
||||
subdomain = GeneratedEnvironmentConfig.quiknodeStellarSubdomain,
|
||||
),
|
||||
infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId,
|
||||
tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey,
|
||||
nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey),
|
||||
getBlockCredentials = createGetBlockCredentials(),
|
||||
kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl,
|
||||
tonCenterCredentials = TonCenterCredentials(
|
||||
mainnetApiKey = TonCenterApiKey.mainnet,
|
||||
testnetApiKey = TonCenterApiKey.testnet,
|
||||
),
|
||||
chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey,
|
||||
chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey,
|
||||
hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey,
|
||||
polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey,
|
||||
bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey,
|
||||
bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey,
|
||||
dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey,
|
||||
koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey,
|
||||
alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey,
|
||||
moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey,
|
||||
etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey,
|
||||
blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey,
|
||||
tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGetBlockCredentials(): GetBlockCredentials {
|
||||
return GetBlockCredentials(
|
||||
xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc),
|
||||
cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta),
|
||||
avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc),
|
||||
eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc),
|
||||
etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc),
|
||||
fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc),
|
||||
rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc),
|
||||
bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc),
|
||||
polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc),
|
||||
gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc),
|
||||
cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc),
|
||||
solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc),
|
||||
ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc),
|
||||
tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest),
|
||||
cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest),
|
||||
near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc),
|
||||
aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest),
|
||||
dogecoin = GetBlockAccessToken(
|
||||
jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc,
|
||||
blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest,
|
||||
),
|
||||
litecoin = GetBlockAccessToken(
|
||||
jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc,
|
||||
blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest,
|
||||
),
|
||||
dash = GetBlockAccessToken(
|
||||
jsonRpc = GetBlockAccessTokens.Dash.jsonRpc,
|
||||
blockBookRest = GetBlockAccessTokens.Dash.blockBookRest,
|
||||
),
|
||||
bitcoin = GetBlockAccessToken(
|
||||
jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc,
|
||||
blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest),
|
||||
zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc),
|
||||
polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc),
|
||||
base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc),
|
||||
blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc),
|
||||
filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc),
|
||||
arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc),
|
||||
bitcoinCash = GetBlockAccessToken(
|
||||
jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc,
|
||||
blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest,
|
||||
),
|
||||
kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc),
|
||||
moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc),
|
||||
optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc),
|
||||
polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc),
|
||||
shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc),
|
||||
sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc),
|
||||
telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc),
|
||||
tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest),
|
||||
monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest),
|
||||
stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest),
|
||||
berachain = GetBlockAccessToken(rest = GetBlockAccessTokens.Berachain.rest),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,10 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String,
|
||||
@Json(name = "quiknodeMonadSubdomain") val quiknodeMonadSubdomain: String,
|
||||
@Json(name = "quiknodeMonadApiKey") val quiknodeMonadApiKey: String,
|
||||
@Json(name = "quiknodeBerachainSubdomain") val quiknodeBerachainSubdomain: String,
|
||||
@Json(name = "quiknodeBerachainApiKey") val quiknodeBerachainApiKey: String,
|
||||
@Json(name = "quiknodeStellarSubdomain") val quiknodeStellarSubdomain: String,
|
||||
@Json(name = "quiknodeStellarApiKey") val quiknodeStellarApiKey: String,
|
||||
@Json(name = "nowNodesApiKey") val nowNodesApiKey: String,
|
||||
@Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?,
|
||||
@Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys,
|
||||
|
|
@ -101,6 +105,7 @@ data class GetBlockAccessTokens(
|
|||
@Json(name = "tezos") val tezos: GetBlockToken?,
|
||||
@Json(name = "monad") val monad: GetBlockToken?,
|
||||
@Json(name = "stellar") val stellar: GetBlockToken?,
|
||||
@Json(name = "berachain") val berachain: GetBlockToken?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
@file:Suppress("BooleanPropertyNaming")
|
||||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
|
||||
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Payment account status for storage in the local cache.
|
||||
*
|
||||
* @see [com.tangem.domain.pay.PaymentAccountStatus]
|
||||
*/
|
||||
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
|
||||
sealed interface PaymentAccountStatusDM {
|
||||
|
||||
@NameLabel("not_created")
|
||||
data class NotCreated(
|
||||
@Json(name = "not_created") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
|
||||
@NameLabel("kyc_status")
|
||||
data class UnderReview(
|
||||
@Json(name = "kyc_status") val kycStatus: KycStatus,
|
||||
) : PaymentAccountStatusDM
|
||||
|
||||
@NameLabel("issuing_card")
|
||||
data class IssuingCard(
|
||||
@Json(name = "issuing_card") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
|
||||
@NameLabel("locked")
|
||||
data class Locked(
|
||||
@Json(name = "locked") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
|
||||
@NameLabel("balance")
|
||||
data class Loaded(
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "last_four_digits") val lastFourDigits: String,
|
||||
@Json(name = "balance") val balance: BigDecimal,
|
||||
@Json(name = "currency_code") val currencyCode: String,
|
||||
@Json(name = "deposit_address") val depositAddress: String?,
|
||||
@Json(name = "is_pin_set") val isPinSet: Boolean,
|
||||
) : PaymentAccountStatusDM
|
||||
|
||||
@NameLabel("card_issue_failed")
|
||||
data class CardIssueFailed(
|
||||
@Json(name = "card_issue_failed") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config
|
|||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
|
|
@ -19,6 +20,7 @@ class ApiConfigTest {
|
|||
|
||||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
private val apiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||
private val environmentConfig = mockk<EnvironmentConfig>()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
|
|
@ -47,7 +49,7 @@ class ApiConfigTest {
|
|||
when (it) {
|
||||
ApiConfig.ID.Express -> {
|
||||
Express(
|
||||
environmentConfigStorage = mockk(),
|
||||
environmentConfig = environmentConfig,
|
||||
expressAuthProvider = mockk(),
|
||||
appVersionProvider = mockk(),
|
||||
appInfoProvider = mockk(),
|
||||
|
|
@ -55,7 +57,7 @@ class ApiConfigTest {
|
|||
}
|
||||
ApiConfig.ID.YieldSupply -> {
|
||||
YieldSupply(
|
||||
environmentConfigStorage = mockk(),
|
||||
environmentConfig = environmentConfig,
|
||||
appVersionProvider = mockk(),
|
||||
authProvider = appAuthProvider,
|
||||
appInfoProvider = mockk(),
|
||||
|
|
@ -70,14 +72,14 @@ class ApiConfigTest {
|
|||
}
|
||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
||||
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
||||
environmentConfig = environmentConfig,
|
||||
appVersionProvider = mockk(),
|
||||
environmentConfigStorage = mockk()
|
||||
)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
||||
environmentConfig = environmentConfig,
|
||||
appVersionProvider = mockk(),
|
||||
environmentConfigStorage = mockk()
|
||||
)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
||||
ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||
ApiConfig.ID.News -> News(
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package com.tangem.datasource.api.common.config.managers
|
||||
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/**
|
||||
* Mock [EnvironmentConfigStorage] implementation for [ProdApiConfigsManagerTest]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
|
||||
|
||||
private val environmentConfig = EnvironmentConfig(
|
||||
express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"),
|
||||
devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"),
|
||||
blockAidApiKey = BLOCK_AID_API_KEY,
|
||||
tangemApiKey = TANGEM_API_KEY,
|
||||
tangemApiKeyDev = TANGEM_API_KEY_DEV,
|
||||
bffStaticToken = TANGEM_PAY_BFF_KEY,
|
||||
bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
|
||||
tangemApiKeyStage = TANGEM_API_KEY_STAGE,
|
||||
yieldModuleApiKey = YIELD_MODULE_KEY,
|
||||
yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV,
|
||||
)
|
||||
|
||||
override suspend fun initialize() = environmentConfig
|
||||
override fun getConfig() = flowOf(environmentConfig)
|
||||
override fun getConfigSync() = environmentConfig
|
||||
|
||||
companion object {
|
||||
const val EXPRESS_API_KEY = "express_api_key"
|
||||
const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
|
||||
const val BLOCK_AID_API_KEY = "block_aid_api_key"
|
||||
const val TANGEM_API_KEY = "tangem_api_key"
|
||||
const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
|
||||
const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key"
|
||||
const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
|
||||
const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
|
||||
const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
|
||||
const val YIELD_MODULE_KEY = "yield_module_api_key"
|
||||
const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,8 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL
|
|||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
|
|
@ -39,7 +37,7 @@ import java.util.TimeZone
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdApiConfigsManagerTest {
|
||||
|
||||
private val environmentConfigStorage = MockEnvironmentConfigStorage()
|
||||
private val environmentConfig = createMockEnvironmentConfig()
|
||||
private val appVersionProvider = mockk<AppVersionProvider>()
|
||||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||
|
|
@ -94,7 +92,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
when (it) {
|
||||
ApiConfig.ID.Express -> {
|
||||
Express(
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
environmentConfig = environmentConfig,
|
||||
expressAuthProvider = expressAuthProvider,
|
||||
appVersionProvider = appVersionProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
|
|
@ -102,7 +100,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
ApiConfig.ID.YieldSupply -> {
|
||||
YieldSupply(
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
environmentConfig = environmentConfig,
|
||||
appVersionProvider = appVersionProvider,
|
||||
authProvider = appAuthProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
|
|
@ -117,14 +115,14 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
|
||||
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
||||
environmentConfig = environmentConfig,
|
||||
appVersionProvider = appVersionProvider,
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
||||
environmentConfig = environmentConfig,
|
||||
appVersionProvider = appVersionProvider,
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig)
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||
ApiConfig.ID.News -> News(
|
||||
|
|
@ -188,9 +186,9 @@ internal class ProdApiConfigsManagerTest {
|
|||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend {
|
||||
if (environment == ApiEnvironment.PROD) {
|
||||
MockEnvironmentConfigStorage.EXPRESS_API_KEY
|
||||
EXPRESS_API_KEY
|
||||
} else {
|
||||
MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY
|
||||
EXPRESS_DEV_API_KEY
|
||||
}
|
||||
},
|
||||
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
|
||||
|
|
@ -237,7 +235,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://yield.tangem.org/",
|
||||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY },
|
||||
"api-key" to ProviderSuspend { YIELD_MODULE_KEY },
|
||||
"card_id" to ProviderSuspend { APP_CARD_ID },
|
||||
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
|
|
@ -426,5 +424,48 @@ internal class ProdApiConfigsManagerTest {
|
|||
const val P2P_API_KEY = "p2p_api_key"
|
||||
const val APP_CARD_ID = "app_card_id"
|
||||
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
||||
|
||||
// Mock config values
|
||||
const val TANGEM_API_KEY = "tangem_api_key"
|
||||
const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
|
||||
const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
|
||||
const val BLOCK_AID_API_KEY = "block_aid_api_key"
|
||||
const val EXPRESS_API_KEY = "express_api_key"
|
||||
const val EXPRESS_DEV_API_KEY = "express_dev_api_key"
|
||||
const val YIELD_MODULE_KEY = "yield_module_key"
|
||||
|
||||
fun createMockEnvironmentConfig(): EnvironmentConfig {
|
||||
return EnvironmentConfig(
|
||||
moonPayApiKey = "moon_pay_api_key",
|
||||
moonPayApiSecretKey = "moon_pay_secret_key",
|
||||
mercuryoWidgetId = "mercuryo_widget_id",
|
||||
mercuryoSecret = "mercuryo_secret",
|
||||
blockchainSdkConfig = mockk(relaxed = true),
|
||||
amplitudeApiKey = "amplitude_api_key",
|
||||
appsFlyerApiKey = "appsflyer_api_key",
|
||||
appsAppId = "apps_app_id",
|
||||
walletConnectProjectId = "wallet_connect_project_id",
|
||||
express = ExpressModel(
|
||||
apiKey = EXPRESS_API_KEY,
|
||||
signVerifierPublicKey = "express_public_key",
|
||||
),
|
||||
devExpress = ExpressModel(
|
||||
apiKey = EXPRESS_DEV_API_KEY,
|
||||
signVerifierPublicKey = "express_dev_public_key",
|
||||
),
|
||||
stakeKitApiKey = STAKE_KIT_API_KEY,
|
||||
p2pApiKey = null,
|
||||
blockAidApiKey = BLOCK_AID_API_KEY,
|
||||
tangemApiKey = TANGEM_API_KEY,
|
||||
tangemApiKeyDev = TANGEM_API_KEY,
|
||||
tangemApiKeyStage = TANGEM_API_KEY,
|
||||
yieldModuleApiKey = YIELD_MODULE_KEY,
|
||||
yieldModuleApiKeyDev = YIELD_MODULE_KEY,
|
||||
bffStaticToken = TANGEM_PAY_BFF_KEY_DEV,
|
||||
bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
|
||||
gaslessTxApiKeyDev = TANGEM_GASLESS_API_KEY,
|
||||
gaslessTxApiKey = TANGEM_GASLESS_API_KEY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ dependencies {
|
|||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.reorderable)
|
||||
api(deps.compose.reorderable)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
|
|
@ -61,12 +61,12 @@ dependencies {
|
|||
api(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.markdown)
|
||||
implementation(deps.haze) {
|
||||
api(deps.haze) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
implementation(deps.haze.materials) {
|
||||
api(deps.haze.materials) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.SpacerWMax
|
|||
import com.tangem.core.ui.components.TangemTextFieldsDefault
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.AppBarWithSearchTestTags
|
||||
|
||||
/**
|
||||
* App bar with close icon and search functionality
|
||||
|
|
@ -135,7 +137,8 @@ private fun CollapsedSearchView(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clickable { onExpandedChange(true) }
|
||||
.padding(end = TangemTheme.dimens.spacing16),
|
||||
.padding(end = TangemTheme.dimens.spacing16)
|
||||
.testTag(AppBarWithSearchTestTags.SEARCH_ICON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -210,7 +213,8 @@ private fun ExpandedSearchView(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(textFieldFocusRequester)
|
||||
.onFocusChanged { onFocusChange(it.hasFocus) },
|
||||
.onFocusChanged { onFocusChange(it.hasFocus) }
|
||||
.testTag(AppBarWithSearchTestTags.TEXT_FIELD),
|
||||
placeholder = {
|
||||
Text(text = placeholderSearchText)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
@file:Suppress("MagicNumber", "UnnecessaryParentheses")
|
||||
package com.tangem.core.ui.components.background
|
||||
|
||||
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import com.tangem.core.ui.shader.TangemShader
|
||||
import com.tangem.core.ui.shader.runtime.buildEffect
|
||||
import kotlin.math.round
|
||||
|
||||
@Composable
|
||||
fun Modifier.shaderBackground(
|
||||
shader: TangemShader,
|
||||
speed: Float = 1f,
|
||||
fallback: () -> Brush = {
|
||||
Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||
},
|
||||
): Modifier {
|
||||
val runtimeEffect = remember(shader) { buildEffect(shader) }
|
||||
var size: Size by remember { mutableStateOf(Size(-1f, -1f)) }
|
||||
val speedModifier = shader.speedModifier
|
||||
|
||||
val time by if (runtimeEffect.isSupported) {
|
||||
var startMillis = remember(shader) { -1L }
|
||||
produceState(0f, speedModifier) {
|
||||
while (true) {
|
||||
withInfiniteAnimationFrameMillis { frameTimeMillis ->
|
||||
if (startMillis < 0) startMillis = frameTimeMillis
|
||||
value = ((frameTimeMillis - startMillis) / 16.6f) / 10f
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remember { mutableFloatStateOf(-1f) }
|
||||
}
|
||||
|
||||
return this then Modifier.onGloballyPositioned {
|
||||
size = Size(it.size.width.toFloat(), it.size.height.toFloat())
|
||||
}.drawBehind {
|
||||
runtimeEffect.update(
|
||||
shader = shader,
|
||||
time = (time * speed * speedModifier).round(3),
|
||||
width = size.width,
|
||||
height = size.height,
|
||||
) // set uniforms for the shaders
|
||||
|
||||
if (runtimeEffect.isReady) {
|
||||
drawRect(brush = runtimeEffect.build())
|
||||
} else {
|
||||
drawRect(brush = fallback())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Float.round(decimals: Int): Float {
|
||||
var multiplier = 1.0f
|
||||
repeat(decimals) { multiplier *= 10 }
|
||||
return round(this * multiplier) / multiplier
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package com.tangem.core.ui.components.background.northernlights
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import android.graphics.BlurMaskFilter
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) {
|
||||
val transition = rememberInfiniteTransition(label = "FluidMeshGradient")
|
||||
|
||||
// ── Circle 1 (left) ──────────────────────────────────────────────────────
|
||||
val color1 by transition.animateColor(
|
||||
initialValue = Color(0xFF3355EE),
|
||||
targetValue = Color(0xFF5577FF),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(4_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "color1",
|
||||
)
|
||||
val x1 by transition.animateFloat(
|
||||
initialValue = 0.05f,
|
||||
targetValue = 0.28f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "x1",
|
||||
)
|
||||
val y1 by transition.animateFloat(
|
||||
initialValue = 0.0f,
|
||||
targetValue = 0.18f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "y1",
|
||||
)
|
||||
|
||||
// ── Circle 2 (right) ─────────────────────────────────────────────────────
|
||||
val color2 by transition.animateColor(
|
||||
initialValue = Color(0xFF7733CC),
|
||||
targetValue = Color(0xFF4455EE),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(1_500),
|
||||
),
|
||||
label = "color2",
|
||||
)
|
||||
val x2 by transition.animateFloat(
|
||||
initialValue = 0.68f,
|
||||
targetValue = 0.92f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "x2",
|
||||
)
|
||||
val y2 by transition.animateFloat(
|
||||
initialValue = 0.02f,
|
||||
targetValue = 0.20f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(5_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(2_000),
|
||||
),
|
||||
label = "y2",
|
||||
)
|
||||
|
||||
// ── Oval (center) ────────────────────────────────────────────────────────
|
||||
val ovalColor by transition.animateColor(
|
||||
initialValue = Color(0xFF5533CC),
|
||||
targetValue = Color(0xFF8844EE),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(7_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(2_500),
|
||||
),
|
||||
label = "ovalColor",
|
||||
)
|
||||
// ── Circle 3 (center) ────────────────────────────────────────────────────
|
||||
val color3 by transition.animateColor(
|
||||
initialValue = Color(0xFF9933BB),
|
||||
targetValue = Color(0xFFBB44DD),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_000, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(3_000),
|
||||
),
|
||||
label = "color3",
|
||||
)
|
||||
val x3 by transition.animateFloat(
|
||||
initialValue = 0.35f,
|
||||
targetValue = 0.58f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(6_500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(1_000),
|
||||
),
|
||||
label = "x3",
|
||||
)
|
||||
val y3 by transition.animateFloat(
|
||||
initialValue = 0.0f,
|
||||
targetValue = 0.15f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(4_500, easing = FastOutSlowInEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
initialStartOffset = StartOffset(500),
|
||||
),
|
||||
label = "y3",
|
||||
)
|
||||
|
||||
var blurRadiusState by remember { mutableFloatStateOf(0f) }
|
||||
val circlePaint1 = remember { Paint() }
|
||||
val circlePaint2 = remember { Paint() }
|
||||
val circlePaint3 = remember { Paint() }
|
||||
val ovalPaint = remember { Paint() }
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f)
|
||||
val circleRadius = size.width * 0.52f
|
||||
|
||||
// Update maskFilter only when blur radius changes meaningfully
|
||||
if (blurRadiusState != blurRadius) {
|
||||
blurRadiusState = blurRadius
|
||||
val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL)
|
||||
circlePaint1.asFrameworkPaint().maskFilter = mf
|
||||
circlePaint2.asFrameworkPaint().maskFilter = mf
|
||||
circlePaint3.asFrameworkPaint().maskFilter = mf
|
||||
ovalPaint.asFrameworkPaint().maskFilter = mf
|
||||
}
|
||||
|
||||
circlePaint1.color = color1.copy(alpha = 0.85f)
|
||||
circlePaint2.color = color2.copy(alpha = 0.85f)
|
||||
circlePaint3.color = color3.copy(alpha = 0.85f)
|
||||
ovalPaint.color = ovalColor.copy(alpha = 0.80f)
|
||||
|
||||
drawIntoCanvas { canvas ->
|
||||
canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1)
|
||||
canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2)
|
||||
canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3)
|
||||
|
||||
val halfW = size.width * 0.68f
|
||||
val halfH = size.width * 0.24f
|
||||
val ovalCx = size.width * 0.50f
|
||||
val ovalCy = 0f
|
||||
|
||||
canvas.drawOval(
|
||||
Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH),
|
||||
ovalPaint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.core.ui.components.background.northernlights
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.StartOffset
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.keyframes
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.background.shaderBackground
|
||||
import com.tangem.core.ui.res.LocalPowerSavingState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader
|
||||
|
||||
/**
|
||||
* Animated northern lights background.
|
||||
* Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode.
|
||||
*/
|
||||
@Composable
|
||||
fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) {
|
||||
val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||
if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) {
|
||||
NorthernLightsBackgroundWithShader(modifier)
|
||||
} else {
|
||||
MovingColorfulBlubsBackground(modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) {
|
||||
val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2")
|
||||
val backgroundColor = TangemTheme.colors2.surface.level1
|
||||
|
||||
// Each track cycles through 4 states (matching the screenshot frames):
|
||||
// deep/dark → saturated+bright → light/pastel → vibrant/vivid → back
|
||||
// 16 s total per track, staggered so no two tracks peak simultaneously.
|
||||
|
||||
// ── Color 1 – indigo → bright blue → lavender → hot violet ──────────────
|
||||
val color1 by transition.animateColor(
|
||||
initialValue = Color(0xFF2A1480),
|
||||
targetValue = Color(0xFF2A1480),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF2A1480) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "color1",
|
||||
)
|
||||
|
||||
// ── Color 2 – dark blue → cyan-blue → sky → teal ─────────────────────────
|
||||
val color2 by transition.animateColor(
|
||||
initialValue = Color(0xFF1444AA),
|
||||
targetValue = Color(0xFF1444AA),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF1444AA) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(4_000),
|
||||
),
|
||||
label = "color2",
|
||||
)
|
||||
|
||||
// ── Color 3 – dark purple → medium purple → rose pink → magenta ──────────
|
||||
val color3 by transition.animateColor(
|
||||
initialValue = Color(0xFF4422BB),
|
||||
targetValue = Color(0xFF4422BB),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF4422BB) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(8_000),
|
||||
),
|
||||
label = "color3",
|
||||
)
|
||||
|
||||
// ── Color 4 – dark violet → medium violet → light pink → hot pink ────────
|
||||
val color4 by transition.animateColor(
|
||||
initialValue = Color(0xFF331199),
|
||||
targetValue = Color(0xFF331199),
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = 16_000
|
||||
Color(0xFF331199) at 0 using FastOutSlowInEasing
|
||||
Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing
|
||||
Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing
|
||||
Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing
|
||||
},
|
||||
repeatMode = RepeatMode.Restart,
|
||||
initialStartOffset = StartOffset(2_000),
|
||||
),
|
||||
label = "color4",
|
||||
)
|
||||
|
||||
// Keep a stable shader instance so the RuntimeShader is never recreated.
|
||||
// Colors are pushed each recomposition via updateColors().
|
||||
val shader = remember {
|
||||
NorthernLightsMeshGradientShader(
|
||||
colors = arrayOf(
|
||||
Color(0xFF2A1480),
|
||||
Color(0xFF1444AA),
|
||||
Color(0xFF4422BB),
|
||||
Color(0xFF331199),
|
||||
backgroundColor,
|
||||
),
|
||||
speed = 0.5f,
|
||||
scale = 4f,
|
||||
)
|
||||
}
|
||||
val colorsArray = remember { Array(5) { Color.Unspecified } }
|
||||
colorsArray[0] = color1
|
||||
colorsArray[1] = color2
|
||||
colorsArray[2] = color3
|
||||
colorsArray[3] = color4
|
||||
colorsArray[4] = backgroundColor
|
||||
shader.updateColors(colorsArray)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize()
|
||||
.shaderBackground(shader),
|
||||
)
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
fun TangemPullToRefreshContainer(
|
||||
config: PullToRefreshConfig,
|
||||
modifier: Modifier = Modifier,
|
||||
indicatorModifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val state = rememberPullToRefreshState()
|
||||
|
|
@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer(
|
|||
modifier = modifier,
|
||||
indicator = {
|
||||
Indicator(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
modifier = indicatorModifier.align(Alignment.TopCenter),
|
||||
isRefreshing = config.isRefreshing,
|
||||
state = state,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
|
|
|
|||
|
|
@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle {
|
|||
override fun createShader(size: Size): Shader {
|
||||
val center = Offset(size.width / 2f, size.height / 2f)
|
||||
val diagonal = sqrt(size.width * size.width + size.height * size.height)
|
||||
val direction = Offset(x = 1f, y = 0.5f)
|
||||
val halfDist = diagonal / 2f
|
||||
val baseStart = center - direction * halfDist
|
||||
val baseEnd = center + direction * halfDist
|
||||
val shift = direction * offset * diagonal
|
||||
// Subtle diagonal angle, similar to iOS shimmer
|
||||
val direction = Offset(x = 1f, y = 0.3f)
|
||||
|
||||
// Half-width of the blob (80% of diagonal total — wide, soft sweep)
|
||||
val bandHalf = diagonal * 0.40f
|
||||
|
||||
// Sweep the highlight center from left-of-element to right-of-element.
|
||||
// offset 0..1 maps to a full pass including off-screen padding on both sides.
|
||||
val shift = direction * ((offset - 0.5f) * diagonal * 1.5f)
|
||||
val highlightCenter = center + shift
|
||||
|
||||
// Full color text with a wide, gradual low-alpha dip sweeping left → right
|
||||
return LinearGradientShader(
|
||||
colors = listOf(textColor.copy(alpha = 0.2f), textColor),
|
||||
from = baseStart + shift,
|
||||
to = baseEnd + shift,
|
||||
colorStops = listOf(0.0f, 0.15f),
|
||||
tileMode = TileMode.Mirror,
|
||||
colors = listOf(
|
||||
textColor,
|
||||
textColor.copy(alpha = 0.75f),
|
||||
textColor.copy(alpha = 0.45f),
|
||||
textColor.copy(alpha = 0.3f),
|
||||
textColor.copy(alpha = 0.45f),
|
||||
textColor.copy(alpha = 0.75f),
|
||||
textColor,
|
||||
),
|
||||
from = highlightCenter - direction * bandHalf,
|
||||
to = highlightCenter + direction * bandHalf,
|
||||
colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f),
|
||||
tileMode = TileMode.Clamp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,370 @@
|
|||
package com.tangem.core.ui.ds
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val ANIMATION_DURATION = 300
|
||||
private const val MAX_VISIBLE_DOTS = 5
|
||||
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
|
||||
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
|
||||
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
|
||||
|
||||
private val SPACING = 4.dp
|
||||
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
|
||||
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
|
||||
private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
|
||||
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
|
||||
|
||||
/**
|
||||
* // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation.
|
||||
*
|
||||
* A pager indicator that adapts to the number of pages and the current page index.
|
||||
*
|
||||
* For 5 or fewer pages, it shows all dots with the current page highlighted.
|
||||
* For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position.
|
||||
*
|
||||
* @param pagerState state of the pager to observe
|
||||
* @param activeIndicatorColor color for the active page indicator
|
||||
* @param inactiveIndicatorColor color for the inactive page indicators
|
||||
* @param modifier modifier for styling
|
||||
*/
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun TangemPagerIndicator(
|
||||
pagerState: PagerState,
|
||||
modifier: Modifier = Modifier,
|
||||
activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary,
|
||||
inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary,
|
||||
) {
|
||||
val totalPages = pagerState.pageCount
|
||||
val currentIndex = pagerState.currentPage
|
||||
|
||||
if (totalPages == 0) return
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
|
||||
|
||||
var displayLower by remember { mutableIntStateOf(targetLower) }
|
||||
var displayUpper by remember { mutableIntStateOf(targetUpper) }
|
||||
var prevTargetLower by remember { mutableIntStateOf(targetLower) }
|
||||
|
||||
val slideOffset = remember { Animatable(0f) }
|
||||
var isSliding by remember { mutableStateOf(false) }
|
||||
var slideDirection by remember { mutableIntStateOf(0) }
|
||||
val fadeProgress = remember { Animatable(0f) }
|
||||
var fadeJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
LaunchedEffect(targetLower) {
|
||||
if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
|
||||
fadeJob?.cancel()
|
||||
slideOffset.stop()
|
||||
fadeProgress.stop()
|
||||
|
||||
val dir = if (targetLower > prevTargetLower) 1 else -1
|
||||
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
|
||||
val halfEdge = edgeDotSize / 2
|
||||
|
||||
isSliding = true
|
||||
slideDirection = dir
|
||||
fadeProgress.snapTo(0f)
|
||||
|
||||
if (dir > 0) {
|
||||
displayLower = prevTargetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(halfEdge)
|
||||
} else {
|
||||
displayLower = targetLower
|
||||
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
|
||||
slideOffset.snapTo(-halfEdge)
|
||||
}
|
||||
|
||||
prevTargetLower = targetLower
|
||||
|
||||
fadeJob = launch {
|
||||
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
|
||||
}
|
||||
slideOffset.animateTo(
|
||||
if (dir > 0) -halfEdge else halfEdge,
|
||||
tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
displayLower = targetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(0f)
|
||||
isSliding = false
|
||||
slideDirection = 0
|
||||
}
|
||||
}
|
||||
val visibleIndices = (displayLower until displayUpper).toList()
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(slideOffset.value.roundToInt(), 0)
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(SPACING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
visibleIndices.forEach { index ->
|
||||
val dotAlpha = when {
|
||||
!isSliding -> 1f
|
||||
slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
|
||||
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
|
||||
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
|
||||
slideDirection < 0 && index == displayLower -> fadeProgress.value
|
||||
else -> 1f
|
||||
}
|
||||
|
||||
key(index) {
|
||||
Dot(
|
||||
index = index,
|
||||
currentIndex = currentIndex,
|
||||
totalPages = totalPages,
|
||||
activeColor = activeIndicatorColor,
|
||||
inactiveColor = inactiveIndicatorColor,
|
||||
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return 0 to totalPages
|
||||
}
|
||||
val lowerBound = when {
|
||||
currentIndex <= 1 -> 0
|
||||
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
|
||||
else -> currentIndex - 2
|
||||
}
|
||||
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
|
||||
return lowerBound to upperBound
|
||||
}
|
||||
|
||||
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
|
||||
if (index == currentIndex) {
|
||||
return CURRENT_DOT_SIZE
|
||||
}
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return NORMAL_DOT_SIZE
|
||||
}
|
||||
val params = DotSizeParams.create(index, currentIndex, totalPages)
|
||||
return params.calculateSize()
|
||||
}
|
||||
|
||||
private class DotSizeParams private constructor(
|
||||
val posInWindow: Int,
|
||||
val currentPosInWindow: Int,
|
||||
val hiddenLeft: Int,
|
||||
val hiddenRight: Int,
|
||||
val distanceFromCurrent: Int,
|
||||
) {
|
||||
private val lastPos = MAX_VISIBLE_DOTS - 1
|
||||
private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1
|
||||
|
||||
fun calculateSize(): DpSize = when {
|
||||
isCentered -> getCenteredSize()
|
||||
hiddenRight >= 1 -> getRightEdgeSize()
|
||||
hiddenLeft >= 1 -> getLeftEdgeSize()
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
|
||||
private fun getCenteredSize(): DpSize = when (posInWindow) {
|
||||
0, lastPos -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
|
||||
private fun getRightEdgeSize(): DpSize {
|
||||
val isLastPos = posInWindow == lastPos
|
||||
val isSecondToLast = posInWindow == lastPos - 1
|
||||
val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||
|
||||
return when {
|
||||
isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||
isLastPos && isModerateDistance -> HINT_DOT_SIZE
|
||||
isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLeftEdgeSize(): DpSize {
|
||||
val isFirstPos = posInWindow == 0
|
||||
val isSecondPos = posInWindow == 1
|
||||
val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||
|
||||
return when {
|
||||
isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||
isFirstPos && isModerateDistance -> HINT_DOT_SIZE
|
||||
isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams {
|
||||
val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex)
|
||||
val posInWindow = index - windowStart
|
||||
val currentPosInWindow = currentIndex - windowStart
|
||||
return DotSizeParams(
|
||||
posInWindow = posInWindow,
|
||||
currentPosInWindow = currentPosInWindow,
|
||||
hiddenLeft = windowStart,
|
||||
hiddenRight = totalPages - windowEnd,
|
||||
distanceFromCurrent = abs(posInWindow - currentPosInWindow),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dot(
|
||||
index: Int,
|
||||
currentIndex: Int,
|
||||
totalPages: Int,
|
||||
activeColor: Color,
|
||||
inactiveColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isActive = index == currentIndex
|
||||
val size = getDotSize(index, currentIndex, totalPages)
|
||||
|
||||
val animSpec = tween<Dp>(ANIMATION_DURATION)
|
||||
val colorSpec = tween<Color>(ANIMATION_DURATION)
|
||||
|
||||
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
|
||||
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = if (isActive) activeColor else inactiveColor,
|
||||
animationSpec = colorSpec,
|
||||
label = "c$index",
|
||||
)
|
||||
|
||||
val shape = RoundedCornerShape(animatedHeight / 2)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(animatedColor, shape),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator6ItemsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 6 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator7ItemsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5, 6).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 7 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator10ItemsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page ->
|
||||
TangemPagerIndicator(rememberPagerState(page) { 10 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorSmallCountsPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
TangemPagerIndicator(rememberPagerState(0) { 1 })
|
||||
TangemPagerIndicator(rememberPagerState(1) { 2 })
|
||||
TangemPagerIndicator(rememberPagerState(1) { 3 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,14 +72,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
|
|||
*/
|
||||
@Composable
|
||||
fun TangemBadge(
|
||||
text: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
text: TextReference? = null,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
size: TangemBadgeSize = X9,
|
||||
shape: TangemBadgeShape = TangemBadgeShape.Default,
|
||||
color: TangemBadgeColor = TangemBadgeColor.Gray,
|
||||
type: TangemBadgeType = TangemBadgeType.Solid,
|
||||
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
|
||||
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val iconColor = getIconColor(type = type, color = color)
|
||||
|
|
@ -94,7 +94,7 @@ fun TangemBadge(
|
|||
.clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
|
||||
visible = iconRes != null && iconPosition != TangemBadgeIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "Start Icon Visibility",
|
||||
) {
|
||||
|
|
@ -105,13 +105,18 @@ fun TangemBadge(
|
|||
tint = iconColor,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
style = size.toTextStyle(),
|
||||
maxLines = 1,
|
||||
color = getTextColor(type = type, color = color),
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = text != null,
|
||||
label = "Text Visibility",
|
||||
) {
|
||||
val wrappedText = remember(this) { requireNotNull(text) }
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
style = size.toTextStyle(),
|
||||
maxLines = 1,
|
||||
color = getTextColor(type = type, color = color),
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
|
|
@ -178,14 +183,17 @@ enum class TangemBadgeSize {
|
|||
X4 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
|
||||
TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp)
|
||||
}
|
||||
X6 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
|
||||
TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp)
|
||||
}
|
||||
X9 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
|
||||
TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +230,7 @@ enum class TangemBadgeSize {
|
|||
enum class TangemBadgeIconPosition {
|
||||
Start,
|
||||
End,
|
||||
None,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,6 +249,7 @@ enum class TangemBadgeColor {
|
|||
Blue,
|
||||
Red,
|
||||
Gray,
|
||||
Green,
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
|
|
@ -258,6 +268,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when
|
|||
-> TangemTheme.colors2.markers.iconRed
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
}
|
||||
TangemBadgeColor.Green -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.iconGreen
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
|
|
@ -276,8 +292,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when
|
|||
-> TangemTheme.colors2.markers.textRed
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||
}
|
||||
TangemBadgeColor.Green -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.textGreen
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
|
||||
|
|
@ -286,6 +309,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
|||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
|
||||
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen
|
||||
},
|
||||
)
|
||||
TangemBadgeType.Tinted -> background(
|
||||
|
|
@ -293,6 +317,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
|||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
|
||||
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen
|
||||
},
|
||||
)
|
||||
TangemBadgeType.Outline -> {
|
||||
|
|
@ -301,6 +326,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
|
|||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
|
||||
TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen
|
||||
},
|
||||
shape = shape,
|
||||
width = 1.dp,
|
||||
|
|
@ -320,16 +346,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl
|
|||
.background(TangemTheme.colors2.surface.level1)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
repeat(2) { yIndex ->
|
||||
repeat(3) { yIndex ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
repeat(TangemBadgeType.entries.size) { index ->
|
||||
TangemBadge(
|
||||
text = stringReference("Title"),
|
||||
text = stringReference("Title").takeIf { yIndex < 2 },
|
||||
iconRes = R.drawable.ic_information_24,
|
||||
type = TangemBadgeType.entries[index],
|
||||
color = params,
|
||||
shape = TangemBadgeShape.entries[yIndex % 2],
|
||||
iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
|
||||
iconPosition = TangemBadgeIconPosition.entries[yIndex],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -344,6 +370,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider<TangemBadgeC
|
|||
TangemBadgeColor.Gray,
|
||||
TangemBadgeColor.Blue,
|
||||
TangemBadgeColor.Red,
|
||||
TangemBadgeColor.Green,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -36,6 +37,7 @@ fun GhostTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
|
|||
enabled = buttonUM.isEnabled,
|
||||
size = buttonUM.size,
|
||||
state = buttonUM.state,
|
||||
shape = buttonUM.shape,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +65,7 @@ fun GhostTangemButton(
|
|||
size: TangemButtonSize = TangemButtonSize.X15,
|
||||
state: TangemButtonState = TangemButtonState.Default,
|
||||
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
|
||||
shape: TangemButtonShape = TangemButtonShape.Default,
|
||||
) {
|
||||
val contentColor = when (state) {
|
||||
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
|
||||
|
|
@ -70,7 +73,8 @@ fun GhostTangemButton(
|
|||
}
|
||||
TangemButtonInternal(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.clip(shape = shape.toShape(size)),
|
||||
text = text,
|
||||
contentColor = contentColor,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue