Updated on 2026-08-14
This commit is contained in:
commit
304bf0df87
928 changed files with 23076 additions and 29831 deletions
|
|
@ -73,15 +73,6 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
buildConfigField("String", "BUILD_TYPE", "\"debug\"")
|
||||
}
|
||||
release {
|
||||
buildConfigField("String", "BUILD_TYPE", "\"release\"")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
|
|
@ -152,6 +143,7 @@ dependencies {
|
|||
implementation(projects.domain.manageTokens)
|
||||
implementation(projects.domain.nft)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.offramp)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.promo)
|
||||
implementation(projects.domain.promo.models)
|
||||
|
|
@ -304,6 +296,8 @@ dependencies {
|
|||
implementation(projects.features.tokenRecieve.impl)
|
||||
implementation(projects.features.yieldSupply.api)
|
||||
implementation(projects.features.yieldSupply.impl)
|
||||
implementation(projects.features.approval.api)
|
||||
implementation(projects.features.approval.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
@ -380,6 +374,8 @@ dependencies {
|
|||
implementation(deps.amplitude)
|
||||
implementation(deps.appsflyer)
|
||||
implementation(deps.appsflyer.oaid)
|
||||
implementation(deps.customerio.analytics)
|
||||
implementation(deps.customerio.messaging)
|
||||
implementation("com.android.installreferrer:installreferrer:2.2")
|
||||
implementation(deps.spongecastle.core)
|
||||
implementation(deps.lottie)
|
||||
|
|
|
|||
|
|
@ -146,12 +146,7 @@ abstract class BaseTestCase : TestCase(
|
|||
return ApplicationInjectionExecutionRule(
|
||||
toggleStates = mapOf(
|
||||
"SWAP_REDESIGN_ENABLED" to false,
|
||||
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
||||
"HOT_WALLET_ENABLED" to true,
|
||||
"YIELD_SUPPLY_FEATURE_ENABLED" to true,
|
||||
"ACCOUNTS_FEATURE_ENABLED" to true,
|
||||
"FEED_ENABLED" to true,
|
||||
"GASLESS_TRANSACTIONS_ENABLED" to true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
|
||||
import com.tangem.tap.domain.sdk.mocks.MockContent
|
||||
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
|
|
@ -14,7 +13,6 @@ import io.qameta.allure.kotlin.Allure.step
|
|||
fun BaseTestCase.scanCard(
|
||||
productType: ProductType? = null,
|
||||
mockContent: MockContent? = null,
|
||||
alreadyActivatedDialogIsShown: Boolean = false,
|
||||
isTwinsCard: Boolean = false,
|
||||
) {
|
||||
if (productType != null) {
|
||||
|
|
@ -32,12 +30,6 @@ fun BaseTestCase.scanCard(
|
|||
step("Click on 'Scan card or ring' button") {
|
||||
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
|
||||
}
|
||||
if (alreadyActivatedDialogIsShown) {
|
||||
step("Click on 'This is my wallet' button") {
|
||||
waitForIdle()
|
||||
AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() }
|
||||
}
|
||||
}
|
||||
if (isTwinsCard) {
|
||||
step("Click on 'Continue' button") {
|
||||
onOnboardingScreen { continueButton.clickWithAssertion() }
|
||||
|
|
@ -51,14 +43,12 @@ fun BaseTestCase.scanCard(
|
|||
fun BaseTestCase.openMainScreen(
|
||||
productType: ProductType? = null,
|
||||
mockContent: MockContent? = null,
|
||||
alreadyActivatedDialogIsShown: Boolean = false,
|
||||
isTwinsCard: Boolean = false,
|
||||
) {
|
||||
step("Scan card") {
|
||||
scanCard(
|
||||
productType = productType,
|
||||
mockContent = mockContent,
|
||||
alreadyActivatedDialogIsShown = alreadyActivatedDialogIsShown,
|
||||
isTwinsCard = isTwinsCard,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class DetailsTest : BaseTestCase() {
|
|||
fun wallet2DetailsTest() =
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(productType = ProductType.Wallet2)
|
||||
}
|
||||
onTopBar {
|
||||
step("Open wallet details") {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS
|
|||
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.scenarios.*
|
||||
import com.tangem.scenarios.checkFailedTransactionDialog
|
||||
import com.tangem.scenarios.checkScanWarningDialog
|
||||
import com.tangem.scenarios.openMainScreen
|
||||
import com.tangem.scenarios.synchronizeAddresses
|
||||
import com.tangem.screens.*
|
||||
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.sdk.mocks.MockProvider
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -96,7 +97,7 @@ class FeedbackTest : BaseTestCase() {
|
|||
step("Click 'Next' button") {
|
||||
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert sanding text is displayed") {
|
||||
step("Assert sеnding text is displayed") {
|
||||
onSendConfirmScreen { sendingText.assertIsDisplayed() }
|
||||
}
|
||||
step("Click 'Send' button") {
|
||||
|
|
@ -163,39 +164,4 @@ class FeedbackTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3986")
|
||||
@DisplayName("Send feedback: from scan already used wallet alert dialog")
|
||||
@Test
|
||||
fun sendFeedbackAfterScanAlreadyUsedWalletTest() {
|
||||
val gmailText = "Welcome to Gmail"
|
||||
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
device.uiDevice.pressBack()
|
||||
}
|
||||
).run {
|
||||
step("Set mocks for Wallet2") {
|
||||
MockProvider.setMocks(ProductType.Wallet2)
|
||||
}
|
||||
step("Click on 'Accept' button") {
|
||||
onDisclaimerScreen { acceptButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Get started' button") {
|
||||
onStoriesScreen { getStartedButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on 'Scan card or ring' button") {
|
||||
onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() }
|
||||
}
|
||||
step("Check 'Already used Wallet' dialog") {
|
||||
checkAlreadyUsedWalletDialog()
|
||||
}
|
||||
step("Click on 'Request support' button") {
|
||||
AlreadyUsedWalletDialogPageObject { requestSupportButton.click() }
|
||||
}
|
||||
step("Assert 'Gmail' app is open") {
|
||||
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,10 +30,7 @@ class OnboardingTest : BaseTestCase() {
|
|||
@Test
|
||||
fun shibaBackupScreenTest() {
|
||||
setupHooks().run {
|
||||
scanCard(
|
||||
mockContent = ShibaNoBackupMockContent,
|
||||
alreadyActivatedDialogIsShown = true
|
||||
)
|
||||
scanCard(mockContent = ShibaNoBackupMockContent)
|
||||
checkBackupScreen()
|
||||
}
|
||||
}
|
||||
|
|
@ -54,10 +51,7 @@ class OnboardingTest : BaseTestCase() {
|
|||
@Test
|
||||
fun wallet2BackupScreenTest() {
|
||||
setupHooks().run {
|
||||
scanCard(
|
||||
mockContent = Wallet2NoBackupMockContent,
|
||||
alreadyActivatedDialogIsShown = true
|
||||
)
|
||||
scanCard(mockContent = Wallet2NoBackupMockContent)
|
||||
checkBackupScreen()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ResetCardTest : BaseTestCase() {
|
|||
fun resetWallet2CardWithBackupTest() {
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(productType = ProductType.Wallet2)
|
||||
}
|
||||
step("Open 'Device settings' screen") {
|
||||
openDeviceSettingsScreen()
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class ScanCardTest : BaseTestCase() {
|
|||
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen' on $card") {
|
||||
openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(mockContent = cardType)
|
||||
}
|
||||
step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") {
|
||||
checkMultiCurrencyMainScreen(
|
||||
|
|
@ -119,7 +119,7 @@ class ScanCardTest : BaseTestCase() {
|
|||
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen' on '$card' card") {
|
||||
openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(mockContent = cardType)
|
||||
}
|
||||
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
|
||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
||||
|
|
@ -138,7 +138,7 @@ class ScanCardTest : BaseTestCase() {
|
|||
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen' on '$ring'") {
|
||||
openMainScreen(productType = cardType, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(productType = cardType)
|
||||
}
|
||||
step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") {
|
||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
||||
|
|
@ -175,7 +175,7 @@ class ScanCardTest : BaseTestCase() {
|
|||
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen' on '$card' card") {
|
||||
openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(mockContent = cardType)
|
||||
}
|
||||
step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") {
|
||||
checkMultiCurrencyMainScreen(devicesCount, cardName)
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class WarningTest : BaseTestCase() {
|
|||
}
|
||||
).run {
|
||||
step("Open 'Main' screen") {
|
||||
openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent, alreadyActivatedDialogIsShown = true)
|
||||
openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent)
|
||||
}
|
||||
step("Assert 'Seed phrase' notification icon is displayed") {
|
||||
onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() }
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
40
app/src/debug/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="146"
|
||||
android:viewportHeight="146">
|
||||
|
||||
<!-- Tangem logo -->
|
||||
<group
|
||||
android:scaleX="0.33"
|
||||
android:scaleY="0.33"
|
||||
android:translateX="48.91"
|
||||
android:translateY="48.91">
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
|
||||
</group>
|
||||
|
||||
<!-- Colored triangle in bottom-right corner -->
|
||||
<path
|
||||
android:fillColor="#4CAF50"
|
||||
android:pathData="M146,20 L146,146 L20,146 Z" />
|
||||
|
||||
<!-- DEBUG text (rotated 45 degrees) -->
|
||||
<group
|
||||
android:rotation="-45"
|
||||
android:pivotX="91.0"
|
||||
android:pivotY="91.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M67.0,84.7 L70.6,84.7 Q71.9,84.7,72.5,84.9 Q73.4,85.1,74.0,85.8 Q74.7,86.4,75.0,87.4 Q75.3,88.3,75.3,89.7 Q75.3,90.9,75.0,91.8 Q74.6,92.9,73.9,93.6 Q73.4,94.1,72.5,94.4 Q71.9,94.6,70.8,94.6 L67.0,94.6 Z M69.0,86.3 L69.0,92.9 L70.5,92.9 Q71.3,92.9,71.7,92.8 Q72.2,92.7,72.5,92.4 Q72.8,92.1,73.0,91.4 Q73.2,90.8,73.2,89.6 Q73.2,88.5,73.0,87.9 Q72.8,87.3,72.5,86.9 Q72.1,86.6,71.5,86.4 Q71.1,86.3,69.9,86.3 Z M77.0,94.6 L77.0,84.7 L84.4,84.7 L84.4,86.3 L79.0,86.3 L79.0,88.5 L84.0,88.5 L84.0,90.2 L79.0,90.2 L79.0,92.9 L84.5,92.9 L84.5,94.6 Z M86.2,84.7 L90.2,84.7 Q91.4,84.7,92.0,84.8 Q92.5,84.9,93.0,85.2 Q93.5,85.5,93.8,86.0 Q94.1,86.5,94.1,87.2 Q94.1,87.9,93.7,88.4 Q93.3,89.0,92.7,89.3 Q93.6,89.6,94.1,90.2 Q94.6,90.9,94.6,91.7 Q94.6,92.4,94.2,93.0 Q93.9,93.7,93.4,94.0 Q92.8,94.4,92.0,94.5 Q91.5,94.6,89.6,94.6 L86.2,94.6 Z M88.2,86.3 L88.2,88.6 L89.6,88.6 Q90.7,88.6,91.0,88.6 Q91.5,88.5,91.8,88.2 Q92.1,87.9,92.1,87.4 Q92.1,87.0,91.9,86.7 Q91.6,86.4,91.1,86.4 Q90.8,86.3,89.4,86.3 Z M88.2,90.3 L88.2,92.9 L90.1,92.9 Q91.2,92.9,91.5,92.9 Q91.9,92.8,92.2,92.5 Q92.5,92.1,92.5,91.6 Q92.5,91.2,92.3,90.9 Q92.0,90.5,91.6,90.4 Q91.2,90.3,89.9,90.3 Z M96.2,84.7 L98.2,84.7 L98.2,90.0 Q98.2,91.3,98.3,91.7 Q98.4,92.3,98.9,92.7 Q99.4,93.0,100.2,93.0 Q101.1,93.0,101.5,92.7 Q102.0,92.3,102.0,91.8 Q102.1,91.3,102.1,90.2 L102.1,84.7 L104.1,84.7 L104.1,89.9 Q104.1,91.7,104.0,92.4 Q103.8,93.1,103.4,93.6 Q102.9,94.2,102.2,94.5 Q101.5,94.8,100.3,94.8 Q98.9,94.8,98.1,94.4 Q97.4,94.1,97.0,93.6 Q96.6,93.0,96.4,92.5 Q96.2,91.6,96.2,90.0 Z M110.9,90.9 L110.9,89.3 L115.2,89.3 L115.2,93.2 Q114.5,93.8,113.4,94.3 Q112.2,94.8,110.9,94.8 Q109.4,94.8,108.2,94.1 Q107.1,93.4,106.5,92.2 Q105.9,91.0,105.9,89.6 Q105.9,88.0,106.5,86.8 Q107.2,85.6,108.5,85.0 Q109.4,84.5,110.8,84.5 Q112.7,84.5,113.7,85.3 Q114.7,86.0,115.0,87.4 L113.1,87.8 Q112.8,87.1,112.3,86.6 Q111.7,86.2,110.8,86.2 Q109.5,86.2,108.7,87.0 Q108.0,87.9,108.0,89.5 Q108.0,91.3,108.7,92.2 Q109.5,93.0,110.8,93.0 Q111.4,93.0,112.1,92.8 Q112.7,92.5,113.2,92.2 L113.2,90.9 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
40
app/src/internal/res/drawable/ic_launcher_foreground.xml
Normal file
40
app/src/internal/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="146"
|
||||
android:viewportHeight="146">
|
||||
|
||||
<!-- Tangem logo -->
|
||||
<group
|
||||
android:scaleX="0.33"
|
||||
android:scaleY="0.33"
|
||||
android:translateX="48.91"
|
||||
android:translateY="48.91">
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
|
||||
</group>
|
||||
|
||||
<!-- Colored triangle in bottom-right corner -->
|
||||
<path
|
||||
android:fillColor="#2196F3"
|
||||
android:pathData="M146,20 L146,146 L20,146 Z" />
|
||||
|
||||
<!-- INTERNAL text (rotated 45 degrees) -->
|
||||
<group
|
||||
android:rotation="-45"
|
||||
android:pivotX="91.0"
|
||||
android:pivotY="91.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M56.9,94.6 L56.9,84.7 L58.9,84.7 L58.9,94.6 Z M60.9,94.6 L60.9,84.7 L62.8,84.7 L66.9,91.3 L66.9,84.7 L68.7,84.7 L68.7,94.6 L66.7,94.6 L62.7,88.1 L62.7,94.6 Z M73.1,94.6 L73.1,86.3 L70.1,86.3 L70.1,84.7 L78.0,84.7 L78.0,86.3 L75.1,86.3 L75.1,94.6 Z M79.3,94.6 L79.3,84.7 L86.7,84.7 L86.7,86.3 L81.3,86.3 L81.3,88.5 L86.3,88.5 L86.3,90.2 L81.3,90.2 L81.3,92.9 L86.9,92.9 L86.9,94.6 Z M88.6,94.6 L88.6,84.7 L92.8,84.7 Q94.4,84.7,95.1,84.9 Q95.8,85.2,96.2,85.9 Q96.7,86.6,96.7,87.4 Q96.7,88.6,96.0,89.3 Q95.4,90.0,94.0,90.2 Q94.7,90.6,95.1,91.0 Q95.5,91.5,96.3,92.7 L97.5,94.6 L95.1,94.6 L93.6,92.4 Q92.9,91.3,92.6,91.0 Q92.3,90.7,92.0,90.6 Q91.7,90.4,91.0,90.4 L90.6,90.4 L90.6,94.6 Z M90.6,88.9 L92.0,88.9 Q93.5,88.9,93.8,88.7 Q94.2,88.6,94.4,88.3 Q94.6,88.0,94.6,87.6 Q94.6,87.1,94.3,86.8 Q94.1,86.5,93.6,86.4 Q93.3,86.3,92.1,86.3 L90.6,86.3 Z M98.6,94.6 L98.6,84.7 L100.5,84.7 L104.6,91.3 L104.6,84.7 L106.4,84.7 L106.4,94.6 L104.4,94.6 L100.4,88.1 L100.4,94.6 Z M117.5,94.6 L115.3,94.6 L114.5,92.3 L110.5,92.3 L109.7,94.6 L107.5,94.6 L111.4,84.7 L113.5,84.7 Z M113.8,90.7 L112.4,87.0 L111.1,90.7 Z M118.6,94.6 L118.6,84.8 L120.6,84.8 L120.6,92.9 L125.6,92.9 L125.6,94.6 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -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,51 @@
|
|||
package com.tangem.tap.common.analytics.handlers.customerio
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsUserIdHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
|
||||
|
||||
/**
|
||||
* Customer.io analytics handler.
|
||||
*/
|
||||
class CustomerIoAnalyticsHandler(
|
||||
private val client: CustomerIoAnalyticsClient,
|
||||
) : AnalyticsHandler, AnalyticsUserIdHandler {
|
||||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun setUserId(userId: String) {
|
||||
client.setUserId(userId)
|
||||
}
|
||||
|
||||
override fun clearUserId() {
|
||||
client.clearUserId()
|
||||
}
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
// No-op: product events are not sent to Customer.io.
|
||||
// Triggers are configured to come from Amplitude directly.
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID = "CustomerIO"
|
||||
}
|
||||
|
||||
class Builder : AnalyticsHandlerBuilder {
|
||||
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? {
|
||||
val cdpApiKey = data.config.customerIoCdpApiKey
|
||||
return if (data.logConfig.isCustomerIoLogEnabled) {
|
||||
CustomerIoAnalyticsHandler(client = CustomerIoLogClient())
|
||||
} else if (!cdpApiKey.isNullOrBlank()) {
|
||||
CustomerIoAnalyticsHandler(
|
||||
client = CustomerIoClient(
|
||||
application = data.application,
|
||||
cdpApiKey = cdpApiKey,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,6 +1,5 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.fetcher.SingleAccountListFetcher
|
||||
import com.tangem.domain.account.repository.AccountsCRUDRepository
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
|
|
@ -97,12 +96,10 @@ internal object AccountDomainModule {
|
|||
fun provideIsAccountsModeEnabledUseCase(
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
accountsCRUDRepository: AccountsCRUDRepository,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
): IsAccountsModeEnabledUseCase {
|
||||
return IsAccountsModeEnabledUseCase(
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
crudRepository = accountsCRUDRepository,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,6 +1,5 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
|
||||
import com.tangem.domain.nft.*
|
||||
|
|
@ -25,15 +24,11 @@ internal object NFTDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun providesGetNFTCollectionsUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
nftRepository: NFTRepository,
|
||||
singleAccountListSupplier: SingleAccountListSupplier,
|
||||
accountsFeatureToggles: AccountsFeatureToggles,
|
||||
): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
nftRepository = nftRepository,
|
||||
singleAccountListSupplier = singleAccountListSupplier,
|
||||
accountsFeatureToggles = accountsFeatureToggles,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -19,7 +20,6 @@ import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
|
|||
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations
|
||||
import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||
|
|
@ -39,28 +39,6 @@ import javax.inject.Singleton
|
|||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
internal object TokensDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddCryptoCurrenciesUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleStakingBalanceFetcher: SingleStakingBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): AddCryptoCurrenciesUseCase {
|
||||
return AddCryptoCurrenciesUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleStakingBalanceFetcher = singleStakingBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchPendingTransactionsUseCase(
|
||||
|
|
@ -75,32 +53,6 @@ internal object TokensDomainModule {
|
|||
return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenListUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
currenciesStatusesOperations: BaseCurrencyStatusOperations,
|
||||
): GetTokenListUseCase {
|
||||
return GetTokenListUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
currenciesStatusesOperations = currenciesStatusesOperations,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRemoveCurrencyUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
): RemoveCurrencyUseCase {
|
||||
return RemoveCurrencyUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyUseCase(
|
||||
|
|
@ -113,20 +65,6 @@ internal object TokensDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAllWalletsCryptoCurrencyStatusesUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
currencyStatusOperations: BaseCurrencyStatusOperations,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetAllWalletsCryptoCurrencyStatusesUseCase {
|
||||
return GetAllWalletsCryptoCurrencyStatusesUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
currencyStatusOperations = currencyStatusOperations,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCurrencyWarningsUseCase(
|
||||
|
|
@ -176,34 +114,6 @@ internal object TokensDomainModule {
|
|||
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideToggleTokenListGroupingUseCase(
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ToggleTokenListGroupingUseCase {
|
||||
return ToggleTokenListGroupingUseCase(dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase {
|
||||
return ToggleTokenListSortingUseCase(dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApplyTokenListSortingUseCase(
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ApplyTokenListSortingUseCase {
|
||||
return ApplyTokenListSortingUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCryptoCurrencyActionsUseCase(
|
||||
|
|
@ -308,14 +218,6 @@ internal object TokensDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetWalletTotalBalanceUseCase(
|
||||
currenciesStatusesOperations: BaseCurrencyStatusOperations,
|
||||
): GetWalletTotalBalanceUseCase {
|
||||
return GetWalletTotalBalanceUseCase(currenciesStatusesOperations)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
|
||||
|
|
@ -350,7 +252,7 @@ internal object TokensDomainModule {
|
|||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): BaseCurrencyStatusOperations {
|
||||
return CachedCurrenciesStatusesOperations(
|
||||
return BaseCurrencyStatusOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||
|
|
@ -363,12 +265,6 @@ internal object TokensDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
|
||||
return GetCryptoCurrenciesUseCase(currenciesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletBalanceFetcher(
|
||||
|
|
@ -378,6 +274,7 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletBalanceFetcher {
|
||||
|
|
@ -388,6 +285,7 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@ internal object WalletsDomainModule {
|
|||
return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetWalletIconUseCase(walletsRepository: WalletsRepository): GetWalletIconUseCase {
|
||||
return GetWalletIconUseCase(walletsRepository = walletsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGetSelectedWalletSyncUseCase(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.routing.utils
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.feature.qrscanning.QrScanningComponent
|
||||
import com.tangem.feature.referral.api.ReferralComponent
|
||||
|
|
@ -18,7 +17,6 @@ import com.tangem.features.details.component.DetailsComponent
|
|||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.hotwallet.*
|
||||
import com.tangem.features.kyc.KycComponent
|
||||
|
|
@ -26,7 +24,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
|
|||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
|
||||
import com.tangem.features.nft.component.NFTComponent
|
||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
||||
|
|
@ -67,7 +64,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
|
||||
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
||||
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
||||
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
|
||||
private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen,
|
||||
private val onrampComponentFactory: OnrampComponent.Factory,
|
||||
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
|
||||
|
|
@ -117,7 +113,6 @@ internal class ChildFactory @Inject constructor(
|
|||
private val kycComponentFactory: KycComponent.Factory,
|
||||
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
|
||||
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
) {
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -147,11 +142,7 @@ internal class ChildFactory @Inject constructor(
|
|||
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
|
||||
}
|
||||
|
||||
val mode = when (val portfolio = route.portfolioId) {
|
||||
is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId)
|
||||
is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId)
|
||||
null -> ManageTokensMode.None
|
||||
}
|
||||
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None
|
||||
|
||||
createComponentChild(
|
||||
context = context,
|
||||
|
|
@ -193,39 +184,21 @@ internal class ChildFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
is AppRoute.MarketsTokenDetails -> {
|
||||
if (feedFeatureToggle.isFeedEnabled) {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = FeedEntryRoute.MarketTokenDetails(
|
||||
token = route.token,
|
||||
appCurrency = route.appCurrency,
|
||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||
analyticsParams = route.analyticsParams?.let { params ->
|
||||
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = feedEntryComponentFactory,
|
||||
)
|
||||
} else {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = MarketsTokenDetailsComponent.Params(
|
||||
token = route.token,
|
||||
appCurrency = route.appCurrency,
|
||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||
analyticsParams = route.analyticsParams?.let { params ->
|
||||
MarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = marketsTokenDetailsComponentFactory,
|
||||
)
|
||||
}
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = FeedEntryRoute.MarketTokenDetails(
|
||||
token = route.token,
|
||||
appCurrency = route.appCurrency,
|
||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||
analyticsParams = route.analyticsParams?.let { params ->
|
||||
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
|
||||
blockchain = params.blockchain,
|
||||
source = params.source,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = feedEntryComponentFactory,
|
||||
)
|
||||
}
|
||||
is AppRoute.Onramp -> {
|
||||
createComponentChild(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,4 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="146"
|
||||
android:viewportHeight="146">
|
||||
<group
|
||||
android:scaleX="0.33"
|
||||
android:scaleY="0.33"
|
||||
android:translateX="48.91"
|
||||
android:translateY="48.91">
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
|
||||
</group>
|
||||
</vector>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_launcher_foreground_base" />
|
||||
</layer-list>
|
||||
|
|
|
|||
21
app/src/main/res/drawable/ic_launcher_foreground_base.xml
Normal file
21
app/src/main/res/drawable/ic_launcher_foreground_base.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="146"
|
||||
android:viewportHeight="146">
|
||||
<group
|
||||
android:scaleX="0.33"
|
||||
android:scaleY="0.33"
|
||||
android:translateX="48.91"
|
||||
android:translateY="48.91">
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -2,5 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground_base"/>
|
||||
</adaptive-icon>
|
||||
40
app/src/mocked/res/drawable/ic_launcher_foreground.xml
Normal file
40
app/src/mocked/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="146"
|
||||
android:viewportHeight="146">
|
||||
|
||||
<!-- Tangem logo -->
|
||||
<group
|
||||
android:scaleX="0.33"
|
||||
android:scaleY="0.33"
|
||||
android:translateX="48.91"
|
||||
android:translateY="48.91">
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M103.72,0H42.28C32.59,0 27.75,0 24.05,1.89C20.79,3.55 18.14,6.19 16.49,9.45C14.6,13.15 14.6,17.99 14.6,27.68V37.96H131.4V27.68C131.4,17.99 131.4,13.15 129.51,9.45C127.85,6.19 125.21,3.54 121.96,1.89C118.25,0 113.41,0 103.72,0Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M92.46,70.08H131.4V118.32C131.4,128.01 131.4,132.85 129.51,136.55C127.85,139.81 125.21,142.46 121.95,144.11C118.25,146 113.4,146 103.71,146H92.47L92.46,70.08Z" />
|
||||
<path
|
||||
android:fillColor="#1E1E1E"
|
||||
android:pathData="M14.6,70.08H53.53V146H42.28C32.59,146 27.75,146 24.05,144.11C20.79,142.46 18.15,139.81 16.49,136.55C14.6,132.85 14.6,128.01 14.6,118.32V70.08Z" />
|
||||
</group>
|
||||
|
||||
<!-- Colored triangle in bottom-right corner -->
|
||||
<path
|
||||
android:fillColor="#9C27B0"
|
||||
android:pathData="M146,20 L146,146 L20,146 Z" />
|
||||
|
||||
<!-- MOCKED text (rotated 45 degrees) -->
|
||||
<group
|
||||
android:rotation="-45"
|
||||
android:pivotX="91.0"
|
||||
android:pivotY="91.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M61.2,94.6 L61.2,84.7 L64.2,84.7 L66.0,91.4 L67.8,84.7 L70.8,84.7 L70.8,94.6 L68.9,94.6 L68.9,86.8 L66.9,94.6 L65.0,94.6 L63.1,86.8 L63.1,94.6 Z M72.4,89.7 Q72.4,88.2,72.8,87.1 Q73.2,86.4,73.7,85.8 Q74.3,85.2,75.0,84.9 Q75.9,84.5,77.2,84.5 Q79.3,84.5,80.7,85.9 Q82.0,87.2,82.0,89.6 Q82.0,92.0,80.7,93.4 Q79.4,94.8,77.2,94.8 Q75.0,94.8,73.7,93.4 Q72.4,92.1,72.4,89.7 Z M74.4,89.6 Q74.4,91.3,75.2,92.2 Q76.0,93.0,77.2,93.0 Q78.4,93.0,79.1,92.2 Q79.9,91.3,79.9,89.6 Q79.9,87.9,79.2,87.0 Q78.4,86.2,77.2,86.2 Q75.9,86.2,75.2,87.1 Q74.4,87.9,74.4,89.6 Z M89.9,90.9 L91.8,91.6 Q91.4,93.2,90.3,94.0 Q89.3,94.8,87.7,94.8 Q85.7,94.8,84.5,93.4 Q83.2,92.1,83.2,89.7 Q83.2,87.2,84.5,85.9 Q85.7,84.5,87.8,84.5 Q89.6,84.5,90.8,85.6 Q91.5,86.2,91.8,87.4 L89.8,87.9 Q89.6,87.1,89.1,86.7 Q88.5,86.2,87.7,86.2 Q86.6,86.2,85.9,87.0 Q85.3,87.8,85.3,89.6 Q85.3,91.4,85.9,92.2 Q86.6,93.0,87.7,93.0 Q88.5,93.0,89.1,92.5 Q89.6,92.0,89.9,90.9 Z M93.6,94.6 L93.6,84.7 L95.6,84.7 L95.6,89.1 L99.6,84.7 L102.3,84.7 L98.6,88.5 L102.5,94.6 L99.9,94.6 L97.2,89.9 L95.6,91.6 L95.6,94.6 Z M103.5,94.6 L103.5,84.7 L110.9,84.7 L110.9,86.3 L105.5,86.3 L105.5,88.5 L110.5,88.5 L110.5,90.2 L105.5,90.2 L105.5,92.9 L111.1,92.9 L111.1,94.6 Z M112.8,84.7 L116.4,84.7 Q117.7,84.7,118.3,84.9 Q119.2,85.1,119.8,85.8 Q120.4,86.4,120.8,87.4 Q121.1,88.3,121.1,89.7 Q121.1,90.9,120.8,91.8 Q120.4,92.9,119.7,93.6 Q119.2,94.1,118.3,94.4 Q117.7,94.6,116.5,94.6 L112.8,94.6 Z M114.8,86.3 L114.8,92.9 L116.3,92.9 Q117.1,92.9,117.5,92.8 Q118.0,92.7,118.3,92.4 Q118.6,92.1,118.8,91.4 Q119.0,90.8,119.0,89.6 Q119.0,88.5,118.8,87.9 Q118.6,87.3,118.3,86.9 Q117.9,86.6,117.3,86.4 Q116.9,86.3,115.7,86.3 Z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ import android.os.Bundle
|
|||
import com.tangem.common.routing.bundle.RouteBundleParams
|
||||
import com.tangem.common.routing.bundle.bundle
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
|
|
@ -21,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.pay.TangemPayDetailsConfig
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.tokens.model.details.NavigationAction
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -128,8 +128,8 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
data class ManageTokens(
|
||||
val source: Source,
|
||||
val portfolioId: PortfolioId? = null,
|
||||
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") {
|
||||
val accountId: AccountId? = null,
|
||||
) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${accountId?.value}") {
|
||||
|
||||
/**
|
||||
* Source of launching the screen.
|
||||
|
|
@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route {
|
|||
|
||||
@Serializable
|
||||
data class EditAccount(
|
||||
val account: Account,
|
||||
val account: Account.CryptoPortfolio,
|
||||
) : AppRoute(path = "/edit_account/${account.accountId.value}")
|
||||
|
||||
@Serializable
|
||||
data class AccountDetails(
|
||||
val account: Account,
|
||||
val account: Account.CryptoPortfolio,
|
||||
) : AppRoute(path = "/account_details/${account.accountId.value}")
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.common.ui.account
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class TokensListPortfolioItemConverter(
|
||||
val tokenItemUM: TokenItemState,
|
||||
val isExpanded: Boolean,
|
||||
val isCollapsable: Boolean,
|
||||
val tokens: ImmutableList<PortfolioTokensListItemUM>,
|
||||
val onEmptyAction: PortfolioItemContentUM.Empty.Action? = null,
|
||||
) : Converter<Unit, TokensListItemUM.Portfolio> {
|
||||
|
||||
override fun convert(value: Unit): TokensListItemUM.Portfolio {
|
||||
val content = if (tokens.isEmpty()) {
|
||||
PortfolioItemContentUM.Empty(onEmptyAction)
|
||||
} else {
|
||||
PortfolioItemContentUM.Tokens(tokens)
|
||||
}
|
||||
return TokensListItemUM.Portfolio(
|
||||
tokenItemUM = tokenItemUM,
|
||||
isExpanded = isExpanded,
|
||||
isCollapsable = isCollapsable,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,64 +0,0 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(14.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(32.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp)
|
||||
SpacerH(12.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(6.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp)
|
||||
SpacerH(16.dp)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.common.ui.userwallet.converter
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.domain.models.wallet.UserWalletIcon
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Converter for mapping [UserWalletIcon] to [DeviceIconUM],
|
||||
* which is used for displaying the wallet icon in the UI.
|
||||
*/
|
||||
class WalletIconUMConverter @Inject constructor() : Converter<UserWalletIcon, DeviceIconUM> {
|
||||
|
||||
override fun convert(value: UserWalletIcon): DeviceIconUM = with(value) {
|
||||
fun String.parseHexColor(): Color = try {
|
||||
Color(this.toColorInt())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
Color.Unspecified
|
||||
}
|
||||
|
||||
return when (this) {
|
||||
UserWalletIcon.Hot -> DeviceIconUM.Mobile
|
||||
is UserWalletIcon.Stub ->
|
||||
DeviceIconUM.Stub(cardsCount = this.cardsCount)
|
||||
is UserWalletIcon.Default -> if (isRing) {
|
||||
DeviceIconUM.Ring(
|
||||
mainColor = Color.Unspecified,
|
||||
cardColor = Color.Unspecified,
|
||||
secondCardColor = if (cardsCount > 2) Color.Unspecified else null,
|
||||
)
|
||||
} else {
|
||||
DeviceIconUM.Card(
|
||||
mainColor = Color.Unspecified,
|
||||
secondColor = if (cardsCount > 1) Color.Unspecified else null,
|
||||
thirdColor = if (cardsCount > 2) Color.Unspecified else null,
|
||||
)
|
||||
}
|
||||
is UserWalletIcon.Colored -> if (isRing) {
|
||||
DeviceIconUM.Ring(
|
||||
mainColor = mainColor.parseHexColor(),
|
||||
cardColor = secondColor?.parseHexColor(),
|
||||
secondCardColor = thirdColor?.parseHexColor(),
|
||||
)
|
||||
} else {
|
||||
DeviceIconUM.Card(
|
||||
mainColor = mainColor.parseHexColor(),
|
||||
secondColor = secondColor?.parseHexColor(),
|
||||
thirdColor = thirdColor?.parseHexColor(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,38 +24,10 @@
|
|||
"name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED",
|
||||
"version": "5.32.0"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
"version": "5.31.0"
|
||||
},
|
||||
{
|
||||
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
|
||||
"version": "5.30.0"
|
||||
},
|
||||
{
|
||||
"name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
"version": "5.31.0"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "FEED_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "APP_REDESIGN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_TRANSACTIONS_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_MARKET_LIST_ENABLED",
|
||||
"version": "5.34"
|
||||
|
|
@ -78,5 +43,17 @@
|
|||
{
|
||||
"name": "WALLET_REORDER_FEATURE_ENABLED",
|
||||
"version": "5.34"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_APPROVAL_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "MULTI_ADDRESS_UTXO_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
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