Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-28 15:23:39 +02:00
commit 93c5c5ae84
1089 changed files with 46153 additions and 10867 deletions

View file

@ -1,2 +0,0 @@
---
BUNDLE_PATH: "vendor/bundle"

9
.gitignore vendored
View file

@ -38,19 +38,10 @@ app/src/external/google-services.json
# LayoutInspector files
/captures
# bundler
/vendor
# OS-specific files
.DS_Store
.DS_Store?
# fastlane files
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
# Kotlin Plugin
.kotlin/
find-latest-release-branch.output

View file

@ -75,6 +75,10 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

View file

@ -1,6 +0,0 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

View file

@ -1,220 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
rexml
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.702.0)
aws-sdk-core (3.170.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.62.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.119.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.98.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.211.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-firebase_app_distribution (0.4.2)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.33.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.10.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.16.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-playcustomapp_v1 (0.12.0)
google-apis-core (>= 0.9.1, < 2.a)
google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.0)
google-cloud-storage (1.44.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.3.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.6.3)
jwt (2.6.0)
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (5.0.1)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.17.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
word_wrap (1.0.0)
xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
fastlane
fastlane-plugin-firebase_app_distribution
BUNDLED WITH
2.5.23

View file

@ -139,10 +139,11 @@ abstract class BaseTestCase : TestCase(
return ApplicationInjectionExecutionRule(
toggleStates = mapOf(
"NEW_TOKEN_RECEIVE_ENABLED" to true,
"WALLET_BALANCE_FETCHER_ENABLED" to true,
"SWAP_REDESIGN_ENABLED" to true,
"SWAP_REDESIGN_ENABLED" to false,
"NEW_ONRAMP_MAIN_ENABLED" to true,
"HOT_WALLET_ENABLED" to true
"HOT_WALLET_ENABLED" to true,
"YIELD_SUPPLY_FEATURE_ENABLED" to true,
"ACCOUNTS_FEATURE_ENABLED" to true
)
)
}

View file

@ -8,6 +8,7 @@ object TestConstants {
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
const val BITCOIN_RECIPIENT_ADDRESS = "bc1qt90qc0na7z05nh63kyd78tujfc8vqv6sl7e4a9"
const val CARDANO_ADDRESS =
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
@ -30,6 +31,7 @@ object TestConstants {
const val XRP_ACTIVATED_RECIPIENT_ADDRESS = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"
const val DOGECOIN_RECIPIENT_ADDRESS = "DJQR3bdhBKcFGMHX2BkMCkrMFApNWNzr6V"
const val DOGECOIN_ADDRESS = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaqz"
const val TERRA_RECIPIENT_ADDRESS = "terra148dmp5ccazcwdmrcpvqz5rprnn886kemqen3tj"
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L

View file

@ -9,6 +9,7 @@ import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onActionIsUnavailableDialog
import com.tangem.screens.onDataNotLoadedDialog
import com.tangem.screens.onFailedTransactionDialog
import io.qameta.allure.kotlin.Allure.step
@ -77,3 +78,15 @@ fun BaseTestCase.checkActionIsUnavailableDialog() {
onActionIsUnavailableDialog { okButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkDataNotLoadedDialog() {
step("Assert 'The data has not loaded yet' dialog title is displayed") {
onDataNotLoadedDialog { title.assertIsDisplayed() }
}
step("Assert 'The data has not loaded yet' dialog text is displayed") {
onDataNotLoadedDialog { text.assertIsDisplayed() }
}
step("Assert 'The data has not loaded yet' dialog 'Ok' button is displayed") {
onDataNotLoadedDialog { okButton.assertIsDisplayed() }
}
}

View file

@ -54,6 +54,10 @@ fun BaseTestCase.checkNetworkFeeBlock(currentFeeAmount: String, withFeeSelector:
step("Assert select fee icon is displayed") {
onSendConfirmScreen { selectFeeIcon.assertIsDisplayed() }
}
} else {
step("Assert select fee icon is not displayed") {
onSendConfirmScreen { selectFeeIcon.assertIsNotDisplayed() }
}
}
}
@ -139,3 +143,28 @@ fun BaseTestCase.checkRecentAddressItem(address: String, description: String?) {
}
}
}
fun BaseTestCase.checkCustomFeeTooltip(title: String, tooltip: String) {
step("Click on tooltip icon for '$title'") {
waitForIdle()
onSendSelectNetworkFeeBottomSheet { tooltipIcon(title).performClick() }
}
step("Check '$title' tooltip text") {
onSendSelectNetworkFeeBottomSheet { tooltipText(tooltip).assertIsDisplayed() }
}
step("Click on tooltip icon again to close tooltip") {
onSendSelectNetworkFeeBottomSheet { tooltipIcon(title).performClick() }
}
}
fun BaseTestCase.checkChangesInInputTextField(title: String, newValue: String, addition: String = "") {
step("Click on '$title' input text field") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(title).performClick() }
}
step("Type '$newValue' in '$title' input text field") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(title).performTextReplacement(newValue) }
}
step("Assert '$title' value: '$newValue + $addition'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(title).assertTextContains(newValue + addition) }
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.BaseDialogTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.common.ui.R as CommonUIR
class DataNotLoadedDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DataNotLoadedDialogPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(BaseDialogTestTags.TITLE)
hasText(getResourceString(CommonUIR.string.action_buttons_service_loading_alert_title))
useUnmergedTree = true
}
val text: KNode = child {
hasTestTag(BaseDialogTestTags.TEXT)
hasText(getResourceString(CommonUIR.string.action_buttons_service_loading_alert_message))
useUnmergedTree = true
}
val okButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_ok))
}
}
internal fun BaseTestCase.onDataNotLoadedDialog(function: DataNotLoadedDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -1,42 +0,0 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasText as withText
class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SelectNetworkFeePageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(TopAppBarTestTags.TITLE)
hasText(getResourceString(R.string.common_fee_selector_title))
useUnmergedTree = true
}
val marketSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market)))
useUnmergedTree = true
}
val fastSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast)))
useUnmergedTree = true
}
val readMoreTextBlock: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -40,8 +40,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
val addressTextFieldHint: KNode = child {
hasParent(withTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD))
useUnmergedTree = true
hasTestTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD)
}
fun recipientNetworkCaution(network: String): KNode = child {
@ -95,10 +94,8 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
hasAnyDescendant(withText(description, substring = true))
}
if (isMyWallet) {
hasAnySibling(withText(getResourceString(CoreUiR.string.send_recipient_wallets_title)))
hasAnyDescendant(withText(getResourceString(CoreUiR.string.manage_tokens_network_selector_wallet)))
} else {
hasAnySibling(withText(getResourceString(CoreUiR.string.send_recent_transactions)))
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TRANSACTION_ICON))
}
}
@ -119,8 +116,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
}
val destinationTagTextFieldHint: KNode = child {
hasParent(withTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD))
useUnmergedTree = true
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD)
}
val destinationTagBlockText: KNode = child {

View file

@ -0,0 +1,102 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseBottomSheetTestTags
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
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import androidx.compose.ui.test.hasTestTag as withTestTag
import androidx.compose.ui.test.hasText as withText
class SendSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SendSelectNetworkFeeBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val chooseSpeedTitle: KNode = child {
hasTestTag(BaseBottomSheetTestTags.TITLE)
hasText(getResourceString(R.string.fee_selector_choose_speed_title))
useUnmergedTree = true
}
fun regularFeeSelectorItem(title: String): KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_FEE_ITEM)
hasAnyDescendant(withText(title))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_ICON))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.TOKEN_AMOUNT))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.FIAT_AMOUNT))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.DOT_SIGN))
useUnmergedTree = true
}
val customSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_FEE_ITEM)
hasAnyDescendant(withText(getResourceString(R.string.common_custom)))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_ICON))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_TITLE))
useUnmergedTree = true
}
fun customInputItem(title: String, hasFiatAmount: Boolean = false): KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM)
hasAnyDescendant(withText(title))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TITLE))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TOOLTIP_ICON))
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD))
useUnmergedTree = true
if (hasFiatAmount) {
hasAnyDescendant(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT))
}
}
val nonceInputItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_ITEM)
hasAnyDescendant(withText(getResourceString(R.string.send_nonce)))
hasAnyDescendant(withText(getResourceString(R.string.send_nonce_hint)))
useUnmergedTree = true
}
fun tooltipIcon(title: String): KNode = child {
hasAnySibling(withText(title))
useUnmergedTree = true
}
fun tooltipText(text: String): KNode = child {
hasText(text)
useUnmergedTree = true
}
private fun inputTextField(title: String): KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD)
hasAnySibling(withText(title))
useUnmergedTree = true
}
fun inputTextFieldValue(title: String): KNode = inputTextField(title).child {
hasParent(withTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD))
useUnmergedTree = true
}
val nonceInputTextField: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_TEXT_FIELD)
useUnmergedTree = true
}
val customInputItemFiatAmount: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT)
useUnmergedTree = true
}
val doneButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.common_done))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSendSelectNetworkFeeBottomSheet(function: SendSelectNetworkFeeBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,39 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<SwapSelectNetworkFeeBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(R.string.fee_selector_choose_speed_title))
useUnmergedTree = true
}
val marketSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE)
hasText(getResourceString(R.string.common_fee_selector_option_market))
useUnmergedTree = true
}
val fastSelectorItem: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE)
hasText(getResourceString(R.string.common_fee_selector_option_fast))
useUnmergedTree = true
}
val readMoreTextBlock: KNode = child {
hasTestTag(SelectNetworkFeeBottomSheetTestTags.LEARN_MORE_TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -29,7 +29,12 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
val networkFeeBlock: KNode = child {
hasTestTag(BaseBlockTestTags.BLOCK)
hasTestTag(FeeSelectorBlockTestTags.SELECTOR_BLOCK)
useUnmergedTree = true
}
val selectFeeIcon: KNode = child {
hasTestTag(FeeSelectorBlockTestTags.SELECT_FEE_ICON)
useUnmergedTree = true
}

View file

@ -113,8 +113,8 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Check positions of tokens on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
}
}
}
@ -200,10 +200,10 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Check positions of tokens on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 4).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 3).assertIsDisplayed()
}
}
step("Click 'By Balance' button") {
@ -213,10 +213,10 @@ class OrganizeTokensTest : BaseTestCase() {
}
step("Check positions of tokens by balance on 'Organize tokens' screen") {
onOrganizeTokensScreen {
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 4).assertIsDisplayed()
tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed()
tokenWithTitleAndPosition(polExMaticTitle, 1).assertIsDisplayed()
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed()
}
}
step("Click 'Apply' button") {

View file

@ -84,7 +84,7 @@ class SwapTokenTest : BaseTestCase() {
}
step("Assert 'Providers' block is displayed") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT) {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
providersBlock.assertIsDisplayed()
}
}
@ -210,24 +210,25 @@ class SwapTokenTest : BaseTestCase() {
step("Click on 'Network fee' block") {
onSwapTokenScreen {
flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
networkFeeBlock.clickWithAssertion()
selectFeeIcon.clickWithAssertion()
}
}
}
step("Assert 'Select fee' bottom sheet title is displayed") {
onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
onSwapSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Market' item is displayed") {
onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
printSemanticTree(rootIndex = 1, useUnmergedTree = true)
onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
}
step("Assert 'Fast' item is displayed") {
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
}
step("Assert 'Read more' text block is displayed") {
onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
onSwapSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
}
step("Click on 'Fast' item") {
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
}
step("Assert 'Network fee' block is displayed") {
onSwapTokenScreen {

View file

@ -6,9 +6,11 @@ import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.*
import com.tangem.common.utils.*
import com.tangem.common.utils.assertClipboardTextEquals
import com.tangem.common.utils.clearClipboard
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.*

View file

@ -12,10 +12,6 @@ import com.tangem.scenarios.goToQrCodeBottomSheet
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.screens.onMainScreen
import com.tangem.screens.onSwapStoriesScreen
import com.tangem.screens.onSwapTokenScreen
import com.tangem.screens.onTokenDetailsScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
@ -84,8 +80,8 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
step("Assert 'Buy' button is not dimmed") {
onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
}
step("Assert 'Send' button is dimmed") {
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
step("Assert 'Send' button is not dimmed") {
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
}
step("Assert 'Swap' button is dimmed") {
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
@ -239,7 +235,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
waitForIdle()
onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() }
}
step("Assert 'Receive' button is displayed") {
step("Click on 'Receive' button") {
onTokenDetailsScreen { receiveButton().performClick() }
}
step("Go to QR code bottom sheet") {

View file

@ -0,0 +1,441 @@
package com.tangem.tests.send.feeScreen
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
import com.tangem.common.extensions.SwipeDirection
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeVertical
import com.tangem.common.utils.resetWireMockScenarioState
import com.tangem.common.utils.setWireMockScenarioState
import com.tangem.core.ui.R
import com.tangem.scenarios.*
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 SendFeeScreenTest : BaseTestCase() {
@AllureId("4906")
@DisplayName("Send (Fee screen): check fee block for fee in token")
@Test
fun checkFeeBlockForFeeInTokenTest() {
val tokenName = "TerraClassicUSD"
val scenarioName = "Terra"
val tokenAmount = "1"
val feeAmount = "<$0.01"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send' screen") {
openSendScreen(tokenName, scenarioName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(TERRA_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed without fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false)
}
}
}
@AllureId("4868")
@DisplayName("Send (Fee screen): check fee block for fixed fee")
@Test
fun checkFeeBlockForFixedFeeTest() {
val tokenName = "Polkadot"
val tokenAmount = "1"
val feeAmount = "$0.05"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send' screen") {
openSendScreen(tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed without fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false)
}
step("Click on 'Fee selector' block") {
onSendConfirmScreen { feeSelectorBlock.performClick() }
}
step("Assert 'Fee selector' bottom sheet is not displayed") {
onSwapSelectNetworkFeeBottomSheet { title.assertIsNotDisplayed() }
}
}
}
@AllureId("4869")
@DisplayName("Send (Fee screen): check network fee bottom sheet for EVM networks")
@Test
fun checkNetworkFeeBottomSheetForEvmTest() {
val tokenName = "Ethereum"
val tokenAmount = "0.1"
val feeAmount = "~$1.06"
val fiatFeeAmount = "$1.09"
val newFeeAmount = "~$1.09"
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
val feeUpTo = getResourceString(R.string.send_max_fee)
val feeUpToTooltip = getResourceString(R.string.send_custom_amount_fee_footer)
val feeUpToValue = "0.00042 ETH"
val newFeeUpToValue = "0.00043"
val maxFee = getResourceString(R.string.send_custom_evm_max_fee)
val maxFeeTooltip = getResourceString(R.string.send_custom_evm_max_fee_footer)
val maxFeeValue = "20 GWEI"
val newMaxFeeValue = "21"
val priorityFee = getResourceString(R.string.send_custom_evm_priority_fee)
val priorityFeeTooltip = getResourceString(R.string.send_custom_evm_priority_fee_footer)
val priorityFeeValue = "2 GWEI"
val newPriorityFeeValue = "3"
val gasLimit = getResourceString(R.string.send_gas_limit)
val gasLimitTooltip = getResourceString(R.string.send_gas_limit_footer)
val gasLimitValue = "21,000 "
val newGasLimitValue = "22"
val nonce = getResourceString(R.string.send_nonce)
val nonceTooltip = getResourceString(R.string.send_nonce_footer)
val nonceValue = "1"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send' screen") {
openSendScreen(tokenName)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed with fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)
}
step("Click on fee selector icon") {
onSendConfirmScreen { feeSelectorIcon.performClick() }
}
step("Assert 'Fee selector' bottom sheet title is displayed") {
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsDisplayed() }
}
step("Assert '$marketSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
}
step("Assert '$fastSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
}
step("Assert '$slowSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(slowSelectorItem).assertIsDisplayed() }
}
step("Assert '$marketSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
}
step("Assert 'Custom' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { customSelectorItem.assertIsDisplayed() }
}
step("Click on 'Custom' selector item") {
onSendSelectNetworkFeeBottomSheet { customSelectorItem.performClick() }
}
step("Assert '$feeUpTo' input item is displayed") {
onSendSelectNetworkFeeBottomSheet {
customInputItem(title = feeUpTo, hasFiatAmount = true).assertIsDisplayed()
}
}
step("Assert '$maxFee' input item is displayed") {
onSendSelectNetworkFeeBottomSheet { customInputItem(maxFee).assertIsDisplayed() }
}
step("Assert '$priorityFee' input item is displayed") {
onSendSelectNetworkFeeBottomSheet { customInputItem(priorityFee).assertIsDisplayed() }
}
step("Assert '$gasLimit' input item is displayed") {
onSendSelectNetworkFeeBottomSheet { customInputItem(gasLimit).assertIsDisplayed() }
}
step("Swipe up") {
swipeVertical(SwipeDirection.UP)
}
step("Assert '$nonce' input item is displayed") {
onSendSelectNetworkFeeBottomSheet { nonceInputItem.assertIsDisplayed() }
}
step("Check '$feeUpTo' tooltip") {
checkCustomFeeTooltip(title = feeUpTo, tooltip = feeUpToTooltip)
}
step("Check '$maxFee' tooltip") {
checkCustomFeeTooltip(title = maxFee, tooltip = maxFeeTooltip)
}
step("Check '$priorityFee' tooltip") {
checkCustomFeeTooltip(title = priorityFee, tooltip = priorityFeeTooltip)
}
step("Check '$gasLimit' tooltip") {
checkCustomFeeTooltip(title = gasLimit, tooltip = gasLimitTooltip)
}
step("Check '$nonce' tooltip") {
checkCustomFeeTooltip(title = nonce, tooltip = nonceTooltip)
}
step("Assert '$feeUpTo' value: '$feeUpToValue'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(feeUpTo).assertTextContains(feeUpToValue) }
}
step("Assert '$maxFee' value: '$maxFeeValue'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(maxFee).assertTextContains(maxFeeValue) }
}
step("Assert '$priorityFee' value: '$priorityFeeValue'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(priorityFee).assertTextContains(priorityFeeValue) }
}
step("Assert '$gasLimit' value: '$gasLimitValue'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(gasLimit).assertTextContains(gasLimitValue) }
}
step("Check changes in '$maxFee' input text field") {
checkChangesInInputTextField(title = maxFee, newValue = newMaxFeeValue, addition = " GWEI")
}
step("Check changes in '$priorityFee' input text field") {
checkChangesInInputTextField(title = priorityFee, newValue = newPriorityFeeValue, addition = " GWEI")
}
step("Check changes in '$gasLimit' input text field") {
checkChangesInInputTextField(title = gasLimit, newValue = newGasLimitValue, addition = " ")
}
step("Type '$nonceValue' in '$nonce' text field") {
onSendSelectNetworkFeeBottomSheet {
nonceInputTextField.performClick()
nonceInputTextField.performTextReplacement(nonceValue)
}
}
step("Assert '$nonce' value: '$nonceValue'") {
onSendSelectNetworkFeeBottomSheet { nonceInputTextField.assertTextContains(nonceValue) }
}
step("Check changes in '$feeUpTo' input text field") {
checkChangesInInputTextField(title = feeUpTo, newValue = newFeeUpToValue, addition = " ETH")
}
step("Assert new fiat fee amount: '$fiatFeeAmount'") {
onSendSelectNetworkFeeBottomSheet { customInputItemFiatAmount.assertTextContains(fiatFeeAmount) }
}
step("Click on 'Done' button") {
onSendSelectNetworkFeeBottomSheet { doneButton.performClick() }
}
step("Assert fee block is displayed with new fee amount: '$newFeeAmount'") {
checkNetworkFeeBlock(currentFeeAmount = newFeeAmount, withFeeSelector = true)
}
}
}
@AllureId("4870")
@DisplayName("Send (Fee screen): check network fee bottom sheet for Bitcoin")
@Test
fun checkNetworkFeeBottomSheetForBitcoinTest() {
val tokenName = "Bitcoin"
val tokenAmount = "0.00000001"
val feeAmount = "$2.86"
val fiatFeeAmount = "$0.24"
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
val feeUpTo = getResourceString(R.string.send_max_fee)
val feeUpToValue = "0.0000264 BTC"
val newFeeUpToValue = "0.0000022 BTC"
val satoshi = getResourceString(R.string.send_satoshi_per_byte_title)
val satoshiValue = "2"
val decimalNumber = "2.11"
val newSatoshiValue = "1"
val bitcoinUtxoScenarioName = "bitcoin_utxo"
val bitcoinUtxoScenarioState = "Balance"
val feeScenarioName = "bitcoin_estimate_smart_fee"
val feeScenarioState = "Started"
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(bitcoinUtxoScenarioName)
}
).run {
step("Set WireMock scenario: '$bitcoinUtxoScenarioName' to state: '$bitcoinUtxoScenarioState'") {
setWireMockScenarioState(bitcoinUtxoScenarioName, bitcoinUtxoScenarioState)
}
step("Set WireMock scenario: '$feeScenarioName' to state: '$feeScenarioState'") {
setWireMockScenarioState(feeScenarioName, feeScenarioState)
}
step("Open 'Main' screen") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses()
}
step("Open 'Send address' screen") {
openSendAddressScreen(tokenName, tokenAmount)
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(BITCOIN_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed with fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)
}
step("Click on fee selector icon") {
onSendConfirmScreen { feeSelectorIcon.performClick() }
}
step("Assert 'Choose speed' bottom sheet title is displayed") {
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsDisplayed() }
}
step("Assert '$fastSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
}
step("Assert '$slowSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(slowSelectorItem).assertIsDisplayed() }
}
step("Assert '$marketSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
}
step("Assert 'Custom' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { customSelectorItem.assertIsDisplayed() }
}
step("Click on 'Custom' selector item") {
onSendSelectNetworkFeeBottomSheet { customSelectorItem.performClick() }
}
step("Assert '$feeUpTo' input item is displayed") {
onSendSelectNetworkFeeBottomSheet {
customInputItem(title = feeUpTo, hasFiatAmount = true).assertIsDisplayed()
}
}
step("Assert '$feeUpTo' value: '$feeUpToValue'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(feeUpTo).assertTextContains(feeUpToValue) }
}
step("Assert '$satoshi' input item is displayed") {
onSendSelectNetworkFeeBottomSheet { customInputItem(satoshi).assertIsDisplayed() }
}
step("Click on '$satoshi' input text field") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(satoshi).performClick() }
}
step("Type '$decimalNumber' in '$satoshi' input text field") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(satoshi).performTextReplacement(decimalNumber) }
}
step("Assert '$satoshi' value: '$satoshiValue +  '") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(satoshi).assertTextContains("$satoshiValue ") }
}
step("Check changes in '$satoshi' input text field") {
checkChangesInInputTextField(title = satoshi, newValue = newSatoshiValue, addition = " ")
}
step("Assert new fiat fee amount: '$fiatFeeAmount'") {
onSendSelectNetworkFeeBottomSheet { customInputItemFiatAmount.assertTextContains(fiatFeeAmount) }
}
step("Assert '$feeUpTo' value: '$newFeeUpToValue'") {
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(feeUpTo).assertTextContains(newFeeUpToValue) }
}
step("Click on 'Done' button") {
onSendSelectNetworkFeeBottomSheet { doneButton.performClick() }
}
step("Assert fee block is displayed with new fee amount: '$fiatFeeAmount'") {
checkNetworkFeeBlock(currentFeeAmount = fiatFeeAmount, withFeeSelector = true)
}
}
}
@AllureId("4871")
@DisplayName("Send (Fee screen): check network fee bottom sheet networks with fee in token")
@Test
fun checkNetworkFeeBottomSheetForVeThorTest() {
val tokenName = "VeThor"
val mockState = "Vechain"
val tokenAmount = "0.1"
val feeAmount = "<$0.01"
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
setupHooks(
additionalAfterSection = {
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
resetWireMockScenarioState(QUOTES_API_SCENARIO)
}
).run {
step("Open 'Send' screen") {
openSendScreen(tokenName = tokenName, mockState = mockState)
}
step("Type '$tokenAmount' in input text field") {
onSendScreen {
amountInputTextField.performClick()
amountInputTextField.performTextReplacement(tokenAmount)
}
}
step("Click on 'Next' button") {
onSendAddressScreen { nextButton.clickWithAssertion() }
}
step("Type recipient address") {
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
}
step("Click on 'Next' button") {
onSendScreen { nextButton.clickWithAssertion() }
}
step("Assert fee block is displayed with fee selector") {
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)
}
step("Click on fee selector icon") {
onSendConfirmScreen { feeSelectorIcon.performClick() }
}
step("Assert 'Choose speed' bottom sheet title is displayed") {
onSendSelectNetworkFeeBottomSheet { chooseSpeedTitle.assertIsDisplayed() }
}
step("Assert '$fastSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
}
step("Assert '$slowSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(slowSelectorItem).assertIsDisplayed() }
}
step("Assert '$marketSelectorItem' selector item is displayed") {
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
}
}
}
}

View file

@ -307,6 +307,17 @@
android:host="tangem.onelink.me"
android:scheme="https" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="tangem.com"
android:pathPrefix="/news" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

View file

@ -360,7 +360,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private fun createReduxStore(): Store<AppState> {
return Store(
reducer = { action, state -> appReducer(action, state) },
reducer = { action, state -> appReducer(action, requireNotNull(state)) },
middleware = AppState.getMiddleware(),
state = AppState(
daggerGraphState = DaggerGraphState(

View file

@ -6,9 +6,7 @@ import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
@Suppress("CanBeNonNullable")
fun appReducer(action: Action, state: AppState?): AppState {
requireNotNull(state)
fun appReducer(action: Action, state: AppState): AppState {
if (action is AppAction.RestoreState) return action.state
return AppState(

View file

@ -0,0 +1,11 @@
package com.tangem.tap.core.ui
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.ui.DesignFeatureToggles
import javax.inject.Inject
class DefaultDesignFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager,
) : DesignFeatureToggles {
override val isRedesignEnabled: Boolean = featureTogglesManager.isFeatureEnabled("APP_REDESIGN_ENABLED")
}

View file

@ -4,6 +4,7 @@ import androidx.compose.material3.SnackbarHostState
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler
@ -23,12 +24,14 @@ internal object UiDependenciesModule {
fun provideUiDependencies(
vibratorHapticManager: VibratorHapticManager,
appThemeModeHolder: AppThemeModeHolder,
designFeatureToggles: DesignFeatureToggles,
): UiDependencies {
return object : UiDependencies {
override val vibratorHapticManager = vibratorHapticManager
override val appThemeModeHolder = appThemeModeHolder
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.di.core.ui
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.tap.core.ui.DefaultDesignFeatureToggles
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface CoreUiBindsModule {
@Binds
fun bindDesignFeatureToggles(impl: DefaultDesignFeatureToggles): DesignFeatureToggles
}

View file

@ -115,7 +115,7 @@ object MarketsDomainModule {
return FilterAvailableNetworksForWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
shouldUseNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
excludedBlockchains = excludedBlockchains,
)
}

View file

@ -142,22 +142,6 @@ internal object NFTDomainModule {
return GetWalletNFTEnabledUseCase(walletsRepository)
}
@Provides
@Singleton
fun provideClearNFTCacheUseCase(
nftCleaner: NFTCleaner,
currenciesRepository: CurrenciesRepository,
accountsFeatureToggles: AccountsFeatureToggles,
singleAccountListSupplier: SingleAccountListSupplier,
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(
nftCleaner = nftCleaner,
currenciesRepository = currenciesRepository,
accountsFeatureToggles = accountsFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier,
)
}
@Provides
@Singleton
fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase {

View file

@ -6,39 +6,48 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NewsDomainModule {
@Provides
@Singleton
fun provideGetNewsCategoriesUseCase(repository: NewsRepository): GetNewsCategoriesUseCase {
return GetNewsCategoriesUseCase(repository)
}
@Provides
@Singleton
fun provideObserveNewsDetailsUseCase(repository: NewsRepository): ObserveNewsDetailsUseCase {
return ObserveNewsDetailsUseCase(repository)
}
@Provides
@Singleton
fun provideObserveTrendingNewsUseCase(repository: NewsRepository): ManageTrendingNewsUseCase {
return ManageTrendingNewsUseCase(repository)
}
@Provides
@Singleton
fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase {
return GetNewsListBatchFlowUseCase(repository)
}
@Provides
@Singleton
fun provideFetchTrendingNewsUseCase(repository: NewsRepository): FetchTrendingNewsUseCase {
return FetchTrendingNewsUseCase(repository)
}
@Provides
fun provideMarkArticleAsViewedUseCase(repository: NewsRepository): MarkArticleAsViewedUseCase {
return MarkArticleAsViewedUseCase(repository)
}
@Provides
fun provideToggleArticleLikedUseCase(repository: NewsRepository): ToggleArticleLikedUseCase {
return ToggleArticleLikedUseCase(repository)
}
@Provides
fun provideGetNewsUseCase(repository: NewsRepository): GetNewsUseCase {
return GetNewsUseCase(repository)
}
}

View file

@ -83,4 +83,14 @@ internal object NotificationsDomainModule {
): GetNetworksAvailableForNotificationsUseCase {
return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository)
}
@Provides
@Singleton
fun provideClearApplicationIdUseCase(
pushNotificationsRepository: PushNotificationsRepository,
): ClearApplicationIdUseCase {
return ClearApplicationIdUseCase(
pushNotificationsRepository = pushNotificationsRepository,
)
}
}

View file

@ -94,12 +94,12 @@ internal object StakingDomainModule {
@Singleton
fun provideFetchStakingOptionsUseCase(
stakeKitRepository: StakeKitRepository,
p2pRepository: P2PEthPoolRepository,
p2pEthPoolRepository: P2PEthPoolRepository,
stakingErrorResolver: StakingErrorResolver,
): FetchStakingOptionsUseCase {
return FetchStakingOptionsUseCase(
stakeKitRepository = stakeKitRepository,
p2pRepository = p2pRepository,
p2pEthPoolRepository = p2pEthPoolRepository,
stakingErrorResolver = stakingErrorResolver,
)
}

View file

@ -282,11 +282,13 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
dispatchers: CoroutineDispatcherProvider,
currencyChecksRepository: CurrencyChecksRepository,
): GetBalanceNotEnoughForFeeWarningUseCase {
return GetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
dispatchers = dispatchers,
currencyChecksRepository = currencyChecksRepository,
)
}
@ -294,9 +296,8 @@ internal object TokensDomainModule {
@Singleton
fun provideIsAmountSubtractAvailableUseCase(
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
): IsAmountSubtractAvailableUseCase {
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
return IsAmountSubtractAvailableUseCase(currenciesRepository)
}
@Provides

View file

@ -5,12 +5,18 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.GaslessTransactionRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.transaction.usecase.gasless.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -21,7 +27,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
@Module
@InstallIn(SingletonComponent::class)
internal object TransactionDomainModule {
@ -265,4 +271,126 @@ internal object TransactionDomainModule {
): SendLargeSolanaTransactionUseCase {
return SendLargeSolanaTransactionUseCase(cardSdkConfigRepository, walletManagersFacade)
}
@Provides
@Singleton
fun provideGetAvailableFeeTokensUseCase(
gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository,
): GetAvailableFeeTokensUseCase {
return GetAvailableFeeTokensUseCase(
gaslessTransactionRepository = gaslessTransactionRepository,
currenciesRepository = currenciesRepository,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideGetFeeForGaslessUseCase(
walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository,
getFeeUseCase: GetFeeUseCase,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository,
): GetFeeForGaslessUseCase {
return GetFeeForGaslessUseCase(
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig,
gaslessTransactionRepository = gaslessTransactionRepository,
currenciesRepository = currenciesRepository,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
getFeeUseCase = getFeeUseCase,
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideGetFeeForTokenUseCase(
walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository,
): GetFeeForTokenUseCase {
return GetFeeForTokenUseCase(
gaslessTransactionRepository = gaslessTransactionRepository,
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig,
currenciesRepository = currenciesRepository,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideIsGaslessFeeSupportedForNetwork(
currencyChecksRepository: CurrencyChecksRepository,
): IsGaslessFeeSupportedForNetwork {
return IsGaslessFeeSupportedForNetwork(currencyChecksRepository)
}
@Provides
@Singleton
fun provideCreateAndSendGaslessTransactionUseCase(
walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository,
getSingCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): CreateAndSendGaslessTransactionUseCase {
return CreateAndSendGaslessTransactionUseCase(
walletManagersFacade = walletManagersFacade,
getSingleCryptoCurrencyStatusUseCase = getSingCryptoCurrencyStatusUseCase,
gaslessTransactionRepository = gaslessTransactionRepository,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotWalletSigner = tangemHotWalletSignerFactory::create,
)
}
@Provides
@Singleton
fun provideEstimateFeeForTokenUseCase(
walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository: CurrencyChecksRepository,
): EstimateFeeForTokenUseCase {
return EstimateFeeForTokenUseCase(
gaslessTransactionRepository = gaslessTransactionRepository,
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig,
currenciesRepository = currenciesRepository,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideEstimateFeeForGaslessTxUseCase(
walletManagersFacade: WalletManagersFacade,
gaslessTransactionRepository: GaslessTransactionRepository,
currenciesRepository: CurrenciesRepository,
getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase,
estimateFeeUseCase: EstimateFeeUseCase,
currencyChecksRepository: CurrencyChecksRepository,
): EstimateFeeForGaslessTxUseCase {
return EstimateFeeForGaslessTxUseCase(
gaslessTransactionRepository = gaslessTransactionRepository,
walletManagersFacade = walletManagersFacade,
demoConfig = DemoConfig,
currenciesRepository = currenciesRepository,
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
estimateFeeUseCase = estimateFeeUseCase,
currencyChecksRepository = currencyChecksRepository,
)
}
}

View file

@ -333,10 +333,14 @@ internal object WalletsDomainModule {
fun providesUpdateRemoteWalletsInfoUseCase(
walletsRepository: WalletsRepository,
userWalletsSyncDelegate: UserWalletsSyncDelegate,
userWalletsListRepository: UserWalletsListRepository,
generateWalletNameUseCase: GenerateWalletNameUseCase,
): UpdateRemoteWalletsInfoUseCase {
return UpdateRemoteWalletsInfoUseCase(
walletsRepository = walletsRepository,
userWalletsSyncDelegate = userWalletsSyncDelegate,
generateWalletNameUseCase = generateWalletNameUseCase,
userWalletsListRepository = userWalletsListRepository,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.blockaid.BlockAidGasEstimate
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.quotes.QuotesRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
@ -14,6 +15,8 @@ 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
@Suppress("TooManyFunctions")
@ -234,4 +237,18 @@ internal object YieldSupplyDomainModule {
): YieldSupplyGetAvailabilityUseCase {
return YieldSupplyGetAvailabilityUseCase(yieldSupplyRepository)
}
@Provides
@Singleton
fun provideYieldSupplyPendingProcessorUseCase(
yieldSupplyRepository: YieldSupplyRepository,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
dispatcherProvider: CoroutineDispatcherProvider,
): YieldSupplyPendingTracker {
return YieldSupplyPendingTracker(
yieldSupplyRepository = yieldSupplyRepository,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.io),
)
}
}

View file

@ -141,6 +141,14 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/330'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
@ -261,6 +269,20 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/330'/0'/0/0") to ExtendedPublicKey( // Terra
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey( // Vechain
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
ByteArrayKey(
@ -318,6 +340,20 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/330'/0'/0/0") to ExtendedPublicKey( // Terra
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey( // Vechain
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
@ -371,6 +407,13 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey( // Vechain
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
ByteArrayKey(

View file

@ -21,9 +21,11 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.ClearApplicationIdUseCase
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
import com.tangem.domain.notifications.models.NotificationsError
import com.tangem.domain.onramp.FetchHotCryptoUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.models.StoryContentIds
@ -72,6 +74,7 @@ internal class MainViewModel @Inject constructor(
private val getApplicationIdUseCase: GetApplicationIdUseCase,
private val subscribeOnWalletsUseCase: GetSavedWalletsCountUseCase,
private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase,
private val clearApplicationIdUseCase: ClearApplicationIdUseCase,
private val updateRemoteWalletsInfoUseCase: UpdateRemoteWalletsInfoUseCase,
private val sendPushTokenUseCase: SendPushTokenUseCase,
private val apiConfigsManager: ApiConfigsManager,
@ -104,7 +107,7 @@ internal class MainViewModel @Inject constructor(
launch { fetchStakingOptions() }
launch { initPushNotifications() }
launch(dispatchers.default) { initPushNotifications() }
}
viewModelScope.launch { incrementAppLaunchCounterUseCase() }
@ -398,13 +401,27 @@ internal class MainViewModel @Inject constructor(
private suspend fun initPushNotifications() {
getApplicationIdUseCase()
.onRight { applicationId ->
sendPushTokenUseCase(applicationId = applicationId)
associateWalletsWithApplicationId(applicationId = applicationId)
updateRemoteWalletsInfoUseCase(applicationId = applicationId)
sendPushTokenUseCase(applicationId = applicationId).onLeft { error ->
if (error is NotificationsError.ApplicationIdNotFound) {
clearApplicationIdUseCase()
getApplicationIdUseCase().onRight { newApplicationId ->
associateAndUpdateWallets(applicationId = newApplicationId)
}
} else {
associateAndUpdateWallets(applicationId = applicationId)
}
}.onRight {
associateAndUpdateWallets(applicationId = applicationId)
}
}
.onLeft(Timber::e)
}
private suspend fun associateAndUpdateWallets(applicationId: ApplicationId) {
associateWalletsWithApplicationId(applicationId = applicationId)
updateRemoteWalletsInfoUseCase(applicationId = applicationId)
}
private fun associateWalletsWithApplicationId(applicationId: ApplicationId) {
subscribeOnWalletsUseCase()
.onEach { wallets ->

View file

@ -59,6 +59,17 @@ internal class DefaultAuthProvider(
}
}
override fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String> {
return ProviderSuspend {
when (apiEnvironment.invoke()) {
ApiEnvironment.DEV,
-> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey
else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}")
} ?: error("No gasless tx api config provided")
}
}
private suspend fun getWallets(): List<UserWallet> {
return if (shouldUseNewListRepository) {
userWalletsListRepository.userWalletsSync()

View file

@ -1,7 +1,7 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
internal class DefaultP2PEthPoolAuthProvider(
@ -12,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
?: error("No P2P api keys provided")
return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
}
}

View file

@ -16,6 +16,9 @@ import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
import com.tangem.features.createwalletstart.CreateWalletStartComponent
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
@ -38,15 +41,11 @@ import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyActiveComponent
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.features.yield.supply.api.YieldSupplyEntryComponent
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -118,9 +117,10 @@ internal class ChildFactory @Inject constructor(
private val tangemPayDetailsContainerComponentFactory: TangemPayDetailsContainerComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val kycComponentFactory: KycComponent.Factory,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
private val feedFeatureToggle: FeedFeatureToggle,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -205,21 +205,39 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.MarketsTokenDetails -> {
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,
)
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,
)
}
}
is AppRoute.Onramp -> {
createComponentChild(
@ -312,7 +330,7 @@ internal class ChildFactory @Inject constructor(
params = StakingComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.cryptoCurrency,
yieldId = route.yieldId,
integrationId = route.integrationId,
),
componentFactory = stakingComponentFactory,
)
@ -687,25 +705,25 @@ internal class ChildFactory @Inject constructor(
componentFactory = kycComponentFactory,
)
}
is AppRoute.YieldSupplyPromo -> {
is AppRoute.YieldSupplyEntry -> {
createComponentChild(
context = context,
params = YieldSupplyPromoComponent.Params(
userWalletId = route.userWalletId,
currency = route.cryptoCurrency,
apy = route.apy,
),
componentFactory = yieldSupplyPromoComponentFactory,
)
}
is AppRoute.YieldSupplyActive -> {
createComponentChild(
context = context,
params = YieldSupplyActiveComponent.Params(
params = YieldSupplyEntryComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.cryptoCurrency,
apy = route.apy,
),
componentFactory = yieldSupplyActiveComponentFactory,
componentFactory = yieldSupplyEntryComponentFactory,
)
}
is AppRoute.NewsDetails -> {
createComponentChild(
context = context,
params = FeedEntryRoute.NewsDetail(
articleId = route.newsId,
preselectedArticlesId = listOf(route.newsId),
),
componentFactory = feedEntryComponentFactory,
)
}
}

View file

@ -6,6 +6,8 @@ import com.tangem.common.routing.DeepLinkRoute
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
@ -50,6 +52,8 @@ internal class DeepLinkFactory @Inject constructor(
private val swapDeepLink: SwapDeepLinkHandler.Factory,
private val promoDeepLink: PromoDeeplinkHandler.Factory,
private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory,
private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory,
private val feedFeatureToggle: FeedFeatureToggle,
) {
private val permittedAppRoute = MutableStateFlow(false)
@ -102,7 +106,7 @@ internal class DeepLinkFactory @Inject constructor(
private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) {
when (deeplinkUri.scheme) {
DeepLinkScheme.Https.scheme -> handleHttpDeepLinks(deeplinkUri)
DeepLinkScheme.Https.scheme -> handleHttpDeepLinks(deeplinkUri, coroutineScope)
DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent)
DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri)
else -> {
@ -116,10 +120,18 @@ internal class DeepLinkFactory @Inject constructor(
}
}
private fun handleHttpDeepLinks(deeplinkUri: Uri) {
if (deeplinkUri.host == DeepLinkRoute.PayApp.host && deeplinkUri.path?.startsWith("/pay-app") == true) {
onboardVisaDeepLink.create(deeplinkUri)
return
private fun handleHttpDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) {
if (deeplinkUri.host == DeepLinkRoute.PayApp.host) {
when {
deeplinkUri.path?.startsWith("/pay-app") == true -> {
onboardVisaDeepLink.create(deeplinkUri)
return
}
deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> {
newsDetailsDeepLink.create(coroutineScope, deeplinkUri)
return
}
}
}
}

View file

@ -16,7 +16,7 @@
</style>
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/background_primary</item>
<item name="windowSplashScreenBackground">@color/background_secondary</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/inset_splash</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>

View file

@ -4,6 +4,8 @@ import android.net.Uri
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
@ -81,6 +83,12 @@ class DeepLinkFactoryTest {
private val cardSdkProvider = mockk<CardSdkProvider>(relaxed = true) {
every { sdk.uiVisibility() } returns MutableStateFlow(false)
}
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
@ -103,6 +111,8 @@ class DeepLinkFactoryTest {
swapDeepLink = swapDeepLinkFactory,
promoDeepLink = promoDeepLinkFactory,
onboardVisaDeepLink = onboardVisaDeepLink,
newsDetailsDeepLink = newsDeeplink,
feedFeatureToggle = feedFeatureToggle,
)
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -19,7 +19,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.staking)
implementation(projects.domain.markets.models)
implementation(projects.domain.onramp.models)
implementation(projects.domain.appCurrency.models)

View file

@ -7,6 +7,7 @@ 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
@ -209,8 +210,8 @@ sealed class AppRoute(val path: String) : Route {
data class Staking(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val yieldId: String,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
val integrationId: StakingIntegrationID,
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/${integrationId.value}")
@Serializable
data class PushNotification(
@ -456,16 +457,12 @@ sealed class AppRoute(val path: String) : Route {
data class Kyc(val userWalletId: UserWalletId) : AppRoute(path = "/kyc")
@Serializable
data class YieldSupplyPromo(
data class YieldSupplyEntry(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val apy: String,
) : AppRoute(path = "/yield_supply_promo/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
) : AppRoute(path = "/yield_supply_entry/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
@Serializable
data class YieldSupplyActive(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val apy: String,
) : AppRoute(path = "/yield_supply_active/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId")
}

View file

@ -31,5 +31,9 @@ object TangemBlogUrlBuilder {
data object WhatWalletToChoose : Post {
override val path: String = "mobile-wallet"
}
data object WhatIsTransactionFee : Post {
override val path: String = "what-is-a-transaction-fee-and-why-do-we-need-it"
}
}
}

View file

@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
import java.math.BigDecimal
/**
* Factory for creating mock P2P ETH Pool account responses for testing
* Factory for creating mock P2PEthPool account responses for testing
*/
object MockP2PEthPoolAccountResponseFactory {
@ -35,7 +35,7 @@ object MockP2PEthPoolAccountResponseFactory {
availableToUnstake = stakedAmount,
availableToWithdraw = BigDecimal.ZERO,
exitQueue = P2PEthPoolExitQueueDTO(
total = 0.0,
total = BigDecimal.ZERO,
requests = emptyList(),
),
)
@ -55,7 +55,7 @@ object MockP2PEthPoolAccountResponseFactory {
availableToUnstake = BigDecimal.ZERO,
availableToWithdraw = BigDecimal.ZERO,
exitQueue = P2PEthPoolExitQueueDTO(
total = 0.0,
total = BigDecimal.ZERO,
requests = emptyList(),
),
)

View file

@ -9,20 +9,11 @@
<ID>BooleanPropertyNaming:NotificationUM.kt$NotificationUM.Error.ExceedsBalance$val mergeFeeNetworkName: Boolean = false</ID>
<ID>BooleanPropertyNaming:NotificationsFactory.kt$NotificationsFactory$val showNotification = sendingAmount + feeAmount &gt; balance - minimumRequirement.orZero()</ID>
<ID>BooleanPropertyNaming:TokenReceiveBottomSheetConfig.kt$TokenReceiveBottomSheetConfig$val showMemoDisclaimer: Boolean</ID>
<ID>CanBeNonNullable:AmountBlockV2.kt$onClick: (() -&gt; Unit)? = null</ID>
<ID>CanBeNonNullable:NavigationButtonsBlock.kt$footerText: TextReference?</ID>
<ID>CanBeNonNullable:NavigationButtonsBlock.kt$pairButtons: Pair&lt;NavigationButton, NavigationButton&gt;?</ID>
<ID>CanBeNonNullable:NavigationButtonsBlock.kt$prevButton: NavigationButton?</ID>
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$dustValue: BigDecimal?</ID>
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minAdaValue: BigDecimal?</ID>
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal?</ID>
<ID>CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent?</ID>
<ID>MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -&gt; { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -&gt; { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -&gt; null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) }</ID>
<ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) }</ID>
<ID>NoNameShadowing:NavigationButtonsBlock.kt$navigationUM</ID>
<ID>NoNameShadowing:UserWalletItem.kt$balance</ID>
<ID>NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false</ID>
<ID>NullableToStringCall:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$${id.rawCurrencyId}</ID>
<ID>ReusedModifierInstance:AddTokenContent.kt$AddButton( modifier = modifier.fillMaxWidth(), state = state.button, )</ID>
<ID>UnnecessaryEventHandlerParameter:SendDoneButtons.kt$onShareClick: (String) -&gt; Unit</ID>
<ID>UnnecessaryLet:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$let(::add)</ID>

View file

@ -19,7 +19,6 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.quote.PriceChange
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
class AccountCryptoPortfolioItemStateConverter(
@ -41,14 +40,11 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content {
val subtitle2State = when (fiatBalance.amount.isZero()) {
true -> null
false -> priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
val subtitle2State = priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(this),
@ -120,7 +116,7 @@ class AccountCryptoPortfolioItemStateConverter(
)
}
private fun AccountId.toItemId() = this.userWalletId.stringValue + this.value
private fun AccountId.toItemId() = this.value
private fun BigDecimal.getPriceChangeType(): PriceChangeType = PriceChangeConverter.fromBigDecimal(value = this)

View file

@ -20,5 +20,6 @@ object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.Cryp
isGrayscale = false,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
}

View file

@ -22,21 +22,24 @@ class AccountPortfolioItemUMConverter(
) : Converter<Account, UserWalletItemUM> {
override fun convert(value: Account): UserWalletItemUM {
return with(value) {
UserWalletItemUM(
id = accountId.value,
name = accountName.toUM().value,
information = getInfo(account = this),
balance = getBalanceInfo(),
isEnabled = isEnabled,
endIcon = endIcon,
onClick = onClick,
imageState = getImageState(account = this),
)
return when (value) {
is Account.CryptoPortfolio -> with(value) {
UserWalletItemUM(
id = accountId.value,
name = accountName.toUM().value,
information = getInfo(account = this),
balance = getBalanceInfo(),
isEnabled = isEnabled,
endIcon = endIcon,
onClick = onClick,
imageState = getImageState(account = this),
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
}
private fun getInfo(account: Account): UserWalletItemUM.Information.Loaded = when (account) {
private fun getInfo(account: Account.CryptoPortfolio): UserWalletItemUM.Information.Loaded = when (account) {
is Account.CryptoPortfolio -> {
val text = pluralReference(
R.plurals.common_tokens_count,
@ -47,7 +50,7 @@ class AccountPortfolioItemUMConverter(
}
}
private fun getImageState(account: Account) = when (account) {
private fun getImageState(account: Account.CryptoPortfolio) = when (account) {
is Account.CryptoPortfolio -> UserWalletItemUM.ImageState.Account(
name = account.accountName.toUM().value,
icon = account.icon.toUM(),

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -19,7 +20,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.format.bigdecimal.crypto
@ -66,11 +66,14 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
state = amountState.tokenIconState,
iconSize = 40.dp,
)
ResizableText(
Text(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
autoSize = TextAutoSize.StepBased(
maxFontSize = TangemTheme.typography.h2.fontSize,
),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -18,7 +19,6 @@ import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
@ -128,21 +128,23 @@ private fun AmountBlockV2(
.padding(top = 8.dp)
.weight(1f),
) {
ResizableText(
Text(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.h2.fontSize),
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
ResizableText(
Text(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.body2.fontSize),
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
)
extraContent()

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.expressStatus.state
import androidx.compose.runtime.Composable
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.PersistentList
data class ExpressTransactionsBlockState(
val transactions: PersistentList<ExpressTransactionStateUM>,
val bottomSheetSlot: BottomSheetSlot?,
val dialogSlot: DialogSlot?,
)
data class BottomSheetSlot(
val config: TangemBottomSheetConfig,
val content: @Composable () -> Unit,
)
data class DialogSlot(
val config: TokenDetailsDialogConfig,
val content: @Composable () -> Unit,
)

View file

@ -1,22 +1,24 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
@ -39,23 +41,36 @@ fun ArticleCard(
modifier: Modifier = Modifier,
colors: CardColors = TangemBlockCardColors,
) {
BlockCard(
modifier = modifier,
onClick = onArticleClick,
colors = colors,
) {
if (articleConfigUM.isTrending) {
TrendingArticle(articleConfigUM = articleConfigUM)
} else {
DefaultArticle(articleConfigUM = articleConfigUM)
}
if (articleConfigUM.isTrending) {
TrendingArticle(
articleConfigUM = articleConfigUM,
modifier = modifier,
onArticleClick = onArticleClick,
colors = colors,
)
} else {
DefaultArticle(
articleConfigUM = articleConfigUM,
modifier = modifier,
onArticleClick = onArticleClick,
colors = colors,
)
}
}
@Composable
private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
private fun TrendingArticle(
articleConfigUM: ArticleConfigUM,
onArticleClick: () -> Unit,
colors: CardColors,
modifier: Modifier = Modifier,
) {
Column(
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(colors.containerColor)
.clickable { onArticleClick() }
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
@ -82,6 +97,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
style = TangemTheme.typography.h3,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
)
SpacerH(8.dp)
@ -101,8 +117,54 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
}
@Composable
private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
Column(modifier = Modifier.padding(12.dp)) {
fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
BlockCard(
modifier = modifier,
onClick = onClick,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(vertical = 31.dp, horizontal = 12.dp),
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
contentDescription = stringResourceSafe(R.string.common_show_more),
)
SpacerH(16.dp)
Text(
text = stringResourceSafe(R.string.news_all_news),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringResourceSafe(R.string.news_stay_in_the_loop),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
@Composable
private fun DefaultArticle(
articleConfigUM: ArticleConfigUM,
onArticleClick: () -> Unit,
colors: CardColors,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(colors.containerColor)
.clickable { onArticleClick() }
.padding(12.dp),
) {
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt.resolveReference(),
@ -122,33 +184,46 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
overflow = TextOverflow.Ellipsis,
)
SpacerH(16.dp)
SpacerHMax(modifier = Modifier.padding(bottom = 16.dp))
Tags(tags = articleConfigUM.tags.toImmutableList())
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
val expandIndicator = remember {
ContextualFlowRowOverflow.expandIndicator {
val remainingItems = tags.size - shownItemCount
Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
}
}
ContextualFlowRow(
itemCount = tags.size,
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = MAX_TAGS_COUNT_TO_SHOW + 1,
maxLines = 1,
overflow = expandIndicator,
) { index ->
Label(state = tags[index])
itemVerticalAlignment = Alignment.Bottom,
) {
val displayedTags = if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
tags.take(MAX_TAGS_COUNT_TO_SHOW)
} else {
tags
}
displayedTags.forEach { tag ->
Label(state = tag)
}
if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
val remainingItems = tags.size - MAX_TAGS_COUNT_TO_SHOW
Label(
state = LabelUM(
text = TextReference.Str("${StringsSigns.PLUS}$remainingItems"),
maxLines = 1,
),
)
}
}
}
private const val MAX_TAGS_COUNT_TO_SHOW = 3
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -1,7 +1,10 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -9,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@ -49,6 +53,8 @@ internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = M
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.secondary,
)
}

View file

@ -41,8 +41,9 @@ fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
}
@Composable
fun DefaultLoadingArticle() {
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(modifier = Modifier.padding(12.dp)) {

View file

@ -21,7 +21,7 @@ import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
@Suppress("LargeClass")
@Suppress("LargeClass", "CanBeNonNullable")
object NotificationsFactory {
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
@ -314,12 +314,14 @@ object NotificationsFactory {
}
}
@Suppress("LongParameterList")
fun MutableList<NotificationUM>.addExceedsBalanceNotification(
cryptoCurrencyWarning: CryptoCurrencyWarning?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
shouldMergeFeeNetworkName: Boolean,
onClick: (CryptoCurrency) -> Unit,
onAnalyticsEvent: (CryptoCurrency) -> Unit,
onResetAnalyticsEvent: (CryptoCurrency) -> Unit,
) {
when (cryptoCurrencyWarning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
@ -357,7 +359,7 @@ object NotificationsFactory {
)
onAnalyticsEvent(cryptoCurrencyWarning.currency)
}
else -> Unit
else -> onResetAnalyticsEvent(cryptoCurrencyStatus.currency)
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
package com.tangem.common.ui.tokendetails
import com.tangem.core.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.features.tokendetails.impl.R
/**
* Wallet bottom sheet config
@ -12,7 +12,7 @@ import com.tangem.features.tokendetails.impl.R
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
internal data class TokenDetailsDialogConfig(
data class TokenDetailsDialogConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: DialogContentConfig,
@ -28,7 +28,7 @@ internal data class TokenDetailsDialogConfig(
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val warning: Boolean = false,
val hasWarning: Boolean = false,
)
data class ConfirmHideConfig(
@ -51,7 +51,7 @@ internal data class TokenDetailsDialogConfig(
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.token_details_hide_alert_hide),
onClick = onConfirmClick,
warning = true,
hasWarning = true,
)
}

View file

@ -23,7 +23,8 @@ import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -45,7 +46,7 @@ class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val yieldModuleApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingApyMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
private val yieldSupplyPromoBannerKey: String? = null,
private val promoCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it)
},
@ -74,7 +75,7 @@ class TokenItemStateConverter(
createPromoBannerState(
status = status,
yieldModuleApyMap = yieldModuleApyMap,
yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKey,
promoCryptoCurrencyStatus = promoCryptoCurrencyStatus,
onApyLabelClick = onApyLabelClick,
onYieldPromoCloseClick = onYieldPromoCloseClick,
onYieldPromoShown = onYieldPromoShown,
@ -251,9 +252,9 @@ class TokenItemStateConverter(
stakingApyMap = stakingApyMap,
)
val rewardTypeRes = when (stakingInfo.rewardType) {
Yield.RewardType.APR -> R.string.staking_apr_earn_badge
Yield.RewardType.UNKNOWN,
Yield.RewardType.APY,
RewardType.APR -> R.string.staking_apr_earn_badge
RewardType.UNKNOWN,
RewardType.APY,
null,
-> R.string.yield_module_earn_badge
}
@ -283,12 +284,14 @@ class TokenItemStateConverter(
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
is StakingOption.P2P -> {
// P2P or no balance: use preferred validators
// TODO add p2p logic
null
is StakingOption.P2PEthPool -> {
RewardInfo(
rate = stakingOptions.apy,
type = RewardType.APY,
)
}
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
@ -314,7 +317,7 @@ class TokenItemStateConverter(
return StakingLocalInfo(
rate = rateInfo?.rate,
isActive = stakeKitBalance != null, // todo add p2p check
isActive = stakeKitBalance != null || p2pEthPoolBalance != null,
rewardType = rateInfo?.type,
)
}
@ -401,7 +404,7 @@ class TokenItemStateConverter(
private fun createPromoBannerState(
status: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, BigDecimal>,
yieldSupplyPromoBannerKey: String?,
promoCryptoCurrencyStatus: CryptoCurrencyStatus?,
onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
onYieldPromoCloseClick: (() -> Unit)?,
onYieldPromoShown: ((cryptoCurrency: CryptoCurrency) -> Unit)?,
@ -411,13 +414,15 @@ class TokenItemStateConverter(
if (status.value !is CryptoCurrencyStatus.Loaded) {
return TokenItemState.PromoBannerState.Empty
}
if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() ||
yieldModuleApyMap[token.yieldSupplyKey()] == null
) {
if (promoCryptoCurrencyStatus == null || status.currency.id != promoCryptoCurrencyStatus.currency.id) {
return TokenItemState.PromoBannerState.Empty
}
val yieldSupplyApy =
yieldModuleApyMap[token.yieldSupplyKey()] ?: return TokenItemState.PromoBannerState.Empty
val yieldSupplyApy = yieldModuleApyMap.entries.firstOrNull { (key, _) ->
key.equals(
other = token.yieldSupplyKey(),
ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId),
)
}?.value ?: return TokenItemState.PromoBannerState.Empty
return TokenItemState.PromoBannerState.Content(
title = resourceReference(
@ -469,7 +474,7 @@ class TokenItemStateConverter(
private data class StakingLocalInfo(
val rate: BigDecimal?,
val isActive: Boolean,
val rewardType: Yield.RewardType?,
val rewardType: RewardType?,
)
private data class EarnApyInfo(

View file

@ -35,6 +35,7 @@ class UserWalletItemUMConverter(
private val isClickableIfLocked: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
artwork: UserWalletItemUM.ImageState? = null,
private val mode: InfoField = InfoField.Devices,
) : Converter<UserWallet, UserWalletItemUM> {
private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading
@ -61,17 +62,29 @@ class UserWalletItemUMConverter(
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) {
is UserWallet.Cold -> {
val cardCount = userWallet.getCardsCount() ?: 1
TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
when (mode) {
is InfoField.Devices -> {
val cardCount = userWallet.getCardsCount() ?: 1
TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
}
is InfoField.Tokens -> {
TextReference.PluralRes(
id = R.plurals.common_tokens_count,
count = mode.tokensCount,
formatArgs = wrappedList(mode.tokensCount),
)
}
}
}
is UserWallet.Hot -> {
TextReference.Res(R.string.hw_mobile_wallet)
}
}
return UserWalletItemUM.Information.Loaded(text)
}
@ -103,4 +116,9 @@ class UserWalletItemUMConverter(
}
}
}
sealed interface InfoField {
data object Devices : InfoField
data class Tokens(val tokensCount: Int) : InfoField
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.core.analytics.models
const val IS_NOT_HTTP_ERROR = "Is not http error"
sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
@ -77,7 +79,9 @@ sealed class AnalyticsParam {
data object Backup : ScreensSources("Backup")
data object Onboarding : ScreensSources("Onboarding")
data object LongTap : ScreensSources("Long Tap")
data object Market : ScreensSources("Market")
data object Markets : ScreensSources("Markets")
data object MarketPulse : ScreensSources("Market Pulse")
data object TangemPay : ScreensSources("Tangem Pay")
data object WalletSettings : ScreensSources("Wallet Settings")
data object Upgrade : ScreensSources("Upgrade")
@ -86,6 +90,9 @@ sealed class AnalyticsParam {
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
data object AddNewWallet : ScreensSources("Add New Wallet")
data object CreateWallet : ScreensSources("Create Wallet")
data object NewsList : ScreensSources("News List")
data object NewsLink : ScreensSources("News Link")
data object NewsPage : ScreensSources("News Page")
}
sealed class TxSentFrom(val value: String) {
@ -93,24 +100,28 @@ sealed class AnalyticsParam {
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Send"), TxData
data class Swap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Swap"), TxData
data class Staking(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Staking"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
val permissionType: String,
) : TxSentFrom("Approve"), TxData
@ -118,6 +129,7 @@ sealed class AnalyticsParam {
override val blockchain: String,
override val token: String,
override val feeType: FeeType?,
override val feeToken: String,
) : TxSentFrom("WalletConnect"), TxData
data object Sell : TxSentFrom("Sell")
@ -126,24 +138,28 @@ sealed class AnalyticsParam {
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("NFT"), TxData
data class SendWithSwap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Send&Swap"), TxData
data class Earning(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
override val feeToken: String,
) : TxSentFrom("Earning"), TxData
}
sealed interface TxData {
val blockchain: String
val token: String
val feeToken: String
val feeType: FeeType?
}
@ -267,5 +283,6 @@ sealed class AnalyticsParam {
const val ENS = "ENS"
const val ENS_ADDRESS = "ENS Address"
const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)"
const val FEE_TOKEN = "Fee Token"
}
}

View file

@ -87,6 +87,7 @@ sealed class Basic(
sentFrom.feeType?.value?.let {
this[AnalyticsParam.Key.FEE_TYPE] = it
}
this[AnalyticsParam.Key.FEE_TOKEN] = sentFrom.feeToken
}
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {
this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType

View file

@ -0,0 +1,22 @@
package com.tangem.core.analytics.models
/**
* Marker interface for analytics events that should be sent only once per session.
*
* Events implementing this interface will be tracked by their [oneTimeEventId] to ensure
* they are not sent multiple times during the same application session. Once an event
* with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event
* with the same ID will be ignored.
*
* @see Analytics.send
*/
interface OneTimePerSessionEvent {
/**
* Unique identifier for the one-time event.
*
* This ID is used to track whether the event has already been sent in the current session.
* Events with the same [oneTimeEventId] will only be sent once, even if they are
* different instances of the same event class.
*/
val oneTimeEventId: String
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.*
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.analytics.models.OneTimePerSessionEvent
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@ -31,6 +32,7 @@ object Analytics : GlobalAnalyticsEventHandler {
private val handlers = mutableMapOf<String, AnalyticsHandler>()
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
private val oneEventsPerSession = ConcurrentHashMap<String, Boolean>()
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
private val analyticsMutex = Mutex()
@ -85,6 +87,11 @@ object Analytics : GlobalAnalyticsEventHandler {
override fun send(event: AnalyticsEvent) {
analyticsScope.launch {
if (event is OneTimePerSessionEvent &&
oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null
) {
return@launch
}
event.params = applyParamsInterceptors(event)
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }

View file

@ -0,0 +1,19 @@
package com.tangem.core.analytics.api
import com.tangem.core.analytics.models.AnalyticsEvent
import java.util.concurrent.ConcurrentHashMap
class ResettableOneTimeEventSender(private val analyticsEventHandler: AnalyticsEventHandler) {
private val sentEvents = ConcurrentHashMap<String, AnalyticsEvent>()
fun sendEventOnce(key: String, event: AnalyticsEvent) {
if (sentEvents.putIfAbsent(key, event) == null) {
analyticsEventHandler.send(event)
}
}
fun reset(key: String) {
sentEvents.remove(key)
}
}

View file

@ -59,16 +59,32 @@
"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": "undefined"
"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": "undefined"
}
]

View file

@ -12,6 +12,5 @@
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withWriteTimeout(timeout = it.duration, unit = it.unit) }</ID>
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$apiConfigs + (apiConfig to environment)</ID>
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$val apiConfig = apiConfigs.keys.firstOrNull { it.id.name == id } ?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")</ID>
<ID>UnusedImports:NetworkModule.kt$import com.tangem.datasource.api.common.config.MoonPay</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -18,6 +18,8 @@ interface AuthProvider {
fun getApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String>
fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String>
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/

View file

@ -31,6 +31,7 @@ sealed class ApiConfig {
YieldSupply,
MoonPay,
News,
GaslessTxService,
}
private fun initializeId(): ID {
@ -39,12 +40,13 @@ sealed class ApiConfig {
is TangemTech -> ID.TangemTech
is StakeKit -> ID.StakeKit
is P2PEthPool -> ID.P2PEthPool
is TangemPay -> ID.TangemPay
is TangemPayAuth -> ID.TangemPayAuth
is TangemPay.Bff -> ID.TangemPay
is TangemPay.Auth -> ID.TangemPayAuth
is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply
is MoonPay -> ID.MoonPay
is News -> ID.News
is GaslessTxService -> ID.GaslessTxService
}
}

View file

@ -0,0 +1,66 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
* Gasless transactions [ApiConfig]
*/
internal class GaslessTxService(
private val authProvider: AuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createDevEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
DEBUG_BUILD_TYPE,
-> ApiEnvironment.DEV
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = PROD_BASE_URL,
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = DEV_BASE_URL,
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createHeaders(environment: ApiEnvironment) = buildMap {
putAll(RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
put(
key = "Authorization",
value = ProviderSuspend {
"Bearer ${authProvider.getGaslessServiceApiKey(Provider { environment }).invoke()}"
},
)
}
private companion object {
private const val PROD_BASE_URL = "https://gasless.tangem.org/"
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
}
}

View file

@ -4,12 +4,16 @@ import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.Provider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
* News [ApiConfig]
[REDACTED_AUTHOR]
*/
internal class News(
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
private val authProvider: AuthProvider,
) : ApiConfig() {
@ -52,6 +56,7 @@ internal class News(
apiEnvironment = Provider { environment },
).values,
)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
}
private companion object {

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
import com.tangem.lib.auth.P2PEthPoolAuthProvider
import com.tangem.utils.ProviderSuspend
@ -23,7 +23,7 @@ internal class P2PEthPool(
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
else -> if (P2PEthPoolStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
}
}

View file

@ -1,11 +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.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal class TangemPay(
internal sealed class TangemPay(
private val appVersionProvider: AppVersionProvider,
private val environmentConfigStorage: EnvironmentConfigStorage,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
@ -16,6 +18,8 @@ internal class TangemPay(
createProdEnvironment(),
)
protected abstract fun getBaseUrl(apiEnvironment: ApiEnvironment): String
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
@ -31,24 +35,75 @@ internal class TangemPay(
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://api.dev.us.paera.com/bff-v2/",
headers = createHeaders(),
baseUrl = getBaseUrl(ApiEnvironment.DEV),
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
baseUrl = getBaseUrl(ApiEnvironment.MOCK),
headers = createHeaders(ApiEnvironment.MOCK),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.us.paera.com/bff-v2/",
headers = createHeaders(),
baseUrl = getBaseUrl(ApiEnvironment.PROD),
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createHeaders() = mapOf(
private fun createHeaders(apiEnvironment: ApiEnvironment) = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
)
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
-> environmentConfigStorage.getConfigSync().bffStaticTokenDev
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
-> null
} ?: error("BffStaticToken is not provided for $apiEnvironment")
}
class Bff(
appVersionProvider: AppVersionProvider,
environmentConfigStorage: EnvironmentConfigStorage,
) : TangemPay(appVersionProvider, environmentConfigStorage) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
ApiEnvironment.MOCK -> "[REDACTED_ENV_URL]"
ApiEnvironment.PROD -> "https://api.us.paera.com/bff-v2/"
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
-> error("Unknown environment: $apiEnvironment")
}
}
}
class Auth(
appVersionProvider: AppVersionProvider,
environmentConfigStorage: EnvironmentConfigStorage,
) : TangemPay(appVersionProvider, environmentConfigStorage) {
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"
ApiEnvironment.MOCK -> "[REDACTED_ENV_URL]"
ApiEnvironment.PROD -> "https://api.us.paera.com/"
ApiEnvironment.DEV_2,
ApiEnvironment.DEV_3,
ApiEnvironment.STAGE,
ApiEnvironment.STAGE_2,
-> error("Unknown environment: $apiEnvironment")
}
}
}
}

View file

@ -1,53 +0,0 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.version.AppVersionProvider
internal class TangemPayAuth(
private val appVersionProvider: AppVersionProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs = listOf(
createDevEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
-> ApiEnvironment.DEV
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "https://api.dev.us.paera.com/",
headers = createHeaders(),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.us.paera.com/",
headers = createHeaders(),
)
private fun createHeaders() = mapOf(
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "Android" },
)
}

View file

@ -2,9 +2,7 @@ package com.tangem.datasource.api.ethpool
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest
import com.tangem.datasource.api.ethpool.models.response.*
import retrofit2.http.*
@ -31,13 +29,13 @@ interface P2PEthPoolApi {
* Create unsigned transaction for depositing ETH into a vault.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Deposit parameters (delegator address, vault address, amount)
* @param body Transaction parameters (delegator address, vault address, amount)
*/
@POST("api/v1/staking/pool/{network}/staking/deposit")
suspend fun createDepositTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolDepositRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
@Body body: P2PEthPoolTransactionRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
/**
* Prepare unstake transaction
@ -45,13 +43,13 @@ interface P2PEthPoolApi {
* Create unsigned transaction to initiate unstaking process.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Unstake parameters (staker public key, stake transaction hash)
* @param body Transaction parameters (delegator address, vault address, amount)
*/
@POST("api/v1/staking/pool/{network}/staking/unstake")
suspend fun createUnstakeTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolUnstakeRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
@Body body: P2PEthPoolTransactionRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
/**
* Prepare withdrawal transaction
@ -59,13 +57,13 @@ interface P2PEthPoolApi {
* Create unsigned transaction to withdraw available funds from exit queue.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param body Withdrawal parameters (staker address)
* @param body Transaction parameters (delegator address, vault address, amount)
*/
@POST("api/v1/staking/pool/{network}/staking/withdraw")
suspend fun createWithdrawTransaction(
@Path("network") network: String,
@Body body: P2PEthPoolWithdrawRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
@Body body: P2PEthPoolTransactionRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
/**
* Broadcast signed transaction
@ -96,22 +94,4 @@ interface P2PEthPoolApi {
@Path("delegatorAddress") delegatorAddress: String,
@Path("vaultAddress") vaultAddress: String,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
/**
* Get rewards history
*
* Retrieve historical rewards data for a specific account and vault.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param delegatorAddress Account address that initiated staking
* @param vaultAddress Ethereum address of the vault
* @param period Optional period filter (30, 60, or 90 days)
*/
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards")
suspend fun getRewards(
@Path("network") network: String,
@Path("delegatorAddress") delegatorAddress: String,
@Path("vaultAddress") vaultAddress: String,
@Query("period") period: Int? = null,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolRewardsResponse>>
}

View file

@ -1,19 +0,0 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for creating deposit transaction
*
* Used in: POST /api/v1/staking/pool/{network}/staking/deposit
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolDepositRequest(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "amount")
val amount: Double,
)

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Unified request body for creating staking transactions (deposit, unstake, withdraw)
*
* Used in:
* - POST /api/v1/staking/pool/{network}/staking/deposit
* - POST /api/v1/staking/pool/{network}/staking/unstake
* - POST /api/v1/staking/pool/{network}/staking/withdraw
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolTransactionRequest(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "amount")
val amount: BigDecimal,
)

View file

@ -1,20 +0,0 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for creating unstake transaction
*
* Used in: POST /api/v1/staking/pool/{network}/staking/unstake
*
* Note: Documentation seems to contain Bitcoin-related fields (possibly copy-paste error).
* Using as-is per specification.
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolUnstakeRequest(
@Json(name = "stakerPublicKey")
val stakerPublicKey: String,
@Json(name = "stakeTransactionHash")
val stakeTransactionHash: String,
)

View file

@ -1,15 +0,0 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for creating withdrawal transaction
*
* Used in: POST /api/v1/staking/pool/{network}/staking/withdraw
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolWithdrawRequest(
@Json(name = "stakerAddress")
val stakerAddress: String,
)

View file

@ -34,7 +34,7 @@ data class P2PEthPoolStakeDTO(
@JsonClass(generateAdapter = true)
data class P2PEthPoolExitQueueDTO(
@Json(name = "total")
val total: Double,
val total: BigDecimal,
@Json(name = "requests")
val requests: List<P2PEthPoolExitRequestDTO>,
)
@ -44,11 +44,11 @@ data class P2PEthPoolExitRequestDTO(
@Json(name = "ticket")
val ticket: String,
@Json(name = "totalAssets")
val totalAssets: Double,
val totalAssets: BigDecimal,
@Json(name = "timestamp")
val timestamp: Long,
@Json(name = "withdrawalTimestamp")
val withdrawalTimestamp: Long,
val withdrawalTimestamp: Long?,
@Json(name = "isClaimable")
val isClaimable: Boolean,
)

View file

@ -29,7 +29,7 @@ data class P2PEthPoolBroadcastResponse(
)
/**
* Transaction status from P2P API
* Transaction status from P2PEthPool API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolTxStatusDTO {

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
/**
* Response for POST /api/v1/staking/pool/{network}/staking/deposit
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolDepositResponse(
@Json(name = "amount")
val amount: Double,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "unsignedTransaction")
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
@Json(name = "createdAt")
val createdAt: DateTime,
)

View file

@ -4,9 +4,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Error response structure for P2P.org API
* Error response structure for P2P.org eth pooled API
*
* All P2P API endpoints return errors in this format
* All P2PEthPool API endpoints return errors in this format
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolErrorResponse(

View file

@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
/**
* Unified response wrapper for all P2P.org API responses
*
* All P2P API endpoints return responses in this format:
* All P2PEthPool API endpoints return responses in this format:
* ```json
* {
* "error": null | { code, message, name, errors },

View file

@ -1,31 +0,0 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
/**
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolRewardsResponse(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "rewards")
val rewards: List<P2PEthPoolRewardDTO>,
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolRewardDTO(
@Json(name = "date")
val date: DateTime,
@Json(name = "apy")
val apy: Double,
@Json(name = "balance")
val balance: BigDecimal,
@Json(name = "rewards")
val rewards: BigDecimal,
)

View file

@ -3,14 +3,20 @@ package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
/**
* Response for POST /api/v1/staking/pool/{network}/staking/withdraw
* Unified response for staking transactions (deposit, unstake, withdraw)
*
* Response for:
* - POST /api/v1/staking/pool/{network}/staking/deposit
* - POST /api/v1/staking/pool/{network}/staking/unstake
* - POST /api/v1/staking/pool/{network}/staking/withdraw
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolWithdrawResponse(
data class P2PEthPoolTransactionResponse(
@Json(name = "amount")
val amount: Double,
val amount: BigDecimal,
@Json(name = "vaultAddress")
val vaultAddress: String,
@Json(name = "delegatorAddress")
@ -20,5 +26,5 @@ data class P2PEthPoolWithdrawResponse(
@Json(name = "createdAt")
val createdAt: DateTime,
@Json(name = "tickets")
val tickets: List<String>,
val tickets: List<String>? = null,
)

View file

@ -1,22 +0,0 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response for POST /api/v1/staking/pool/{network}/staking/unstake
*
* Note: Contains Bitcoin-related fields (likely documentation error).
* Using as-is per specification.
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolUnstakeResponse(
@Json(name = "stakerPublicKey")
val stakerPublicKey: String,
@Json(name = "stakeTransactionHash")
val stakeTransactionHash: String,
@Json(name = "unstakeTransactionHex")
val unstakeTransactionHex: String, // unsigned
@Json(name = "unstakeFee")
val unstakeFee: Double,
)

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
/**
* Response for GET /api/v1/staking/pool/{network}/vaults
@ -15,7 +16,7 @@ data class P2PEthPoolVaultsResponse(
)
/**
* Network identifier in P2P API
* Network identifier in P2PEthPool API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolNetworkDTO {
@ -33,15 +34,15 @@ data class P2PEthPoolVaultDTO(
@Json(name = "displayName")
val displayName: String,
@Json(name = "apy")
val apy: Double,
val apy: BigDecimal,
@Json(name = "baseApy")
val baseApy: Double,
val baseApy: BigDecimal,
@Json(name = "capacity")
val capacity: Double,
val capacity: BigDecimal,
@Json(name = "totalAssets")
val totalAssets: Double,
val totalAssets: BigDecimal,
@Json(name = "feePercent")
val feePercent: Double,
val feePercent: BigDecimal,
@Json(name = "isPrivate")
val isPrivate: Boolean,
@Json(name = "isGenesis")

View file

@ -32,6 +32,9 @@ data class ExchangeProvider(
@Json(name = "slippage")
val slippage: BigDecimal?,
@Json(name = "exchangeOnlyWithinSingleAddress")
val isExchangeOnlyWithinSingleAddress: Boolean = false,
)
@JsonClass(generateAdapter = false)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.api.gasless
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.gasless.models.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
interface GaslessTxServiceApi {
@GET("api/v1/tokens")
suspend fun getSupportedTokens(): ApiResponse<GaslessServiceResponse<GaslessSupportedTokens>>
@POST("api/v1/transaction/sign")
suspend fun signGaslessTransaction(
@Body transaction: GaslessTransactionRequest,
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
@GET("api/v1/config/fee-recipient")
suspend fun getFeeRecipient(): ApiResponse<GaslessServiceResponse<GaslessFeeRecipient>>
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessFeeRecipient(
@Json(name = "feeRecipientAddress") val address: String,
)

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessServiceResponse<T>(
@Json(name = "result") val result: T,
@Json(name = "success") val isSuccess: Boolean,
@Json(name = "timestamp") val timestamp: String,
)

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Result of gasless transaction signing.
* Contains the signed transaction data and gas parameters.
*/
@JsonClass(generateAdapter = true)
data class GaslessSignedTransactionResultDTO(
@Json(name = "txHash")
val txHash: String,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessSupportedTokens(
@Json(name = "tokens") val tokens: List<GaslessTokenDTO>,
)

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GaslessTokenDTO(
@Json(name = "tokenAddress") val tokenAddress: String,
@Json(name = "tokenSymbol") val tokenSymbol: String,
@Json(name = "tokenName") val tokenName: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "chainId") val chainId: Int,
@Json(name = "chain") val chain: String,
)

View file

@ -0,0 +1,96 @@
package com.tangem.datasource.api.gasless.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for gasless transaction submission.
* Represents complete transaction with fee delegation metadata.
*/
@JsonClass(generateAdapter = true)
data class GaslessTransactionRequest(
@Json(name = "gaslessTransaction")
val gaslessTransaction: GaslessTransactionData,
@Json(name = "signature")
val signature: String,
@Json(name = "userAddress")
val userAddress: String,
@Json(name = "chainId")
val chainId: Int,
@Json(name = "eip7702auth")
val eip7702Auth: Eip7702AuthorizationDTO? = null,
)
@JsonClass(generateAdapter = true)
data class GaslessTransactionData(
@Json(name = "transaction")
val transaction: TransactionData,
@Json(name = "fee")
val fee: FeeData,
@Json(name = "nonce")
val nonce: String,
)
@JsonClass(generateAdapter = true)
data class TransactionData(
@Json(name = "to")
val to: String,
@Json(name = "value")
val value: String,
@Json(name = "data")
val data: String,
)
@JsonClass(generateAdapter = true)
data class FeeData(
@Json(name = "feeToken")
val feeToken: String,
@Json(name = "maxTokenFee")
val maxTokenFee: String,
@Json(name = "coinPriceInToken")
val coinPriceInToken: String,
@Json(name = "feeTransferGasLimit")
val feeTransferGasLimit: String,
@Json(name = "baseGas")
val baseGas: String,
@Json(name = "feeReceiver")
val feeReceiver: String,
)
/**
* EIP-7702 authorization for account abstraction.
* Optional field, used only when EOA delegation is required.
*/
@JsonClass(generateAdapter = true)
data class Eip7702AuthorizationDTO(
@Json(name = "chainId")
val chainId: Int,
@Json(name = "address")
val address: String,
@Json(name = "nonce")
val nonce: String,
@Json(name = "yParity")
val yParity: Int,
@Json(name = "r")
val r: String,
@Json(name = "s")
val s: String,
)

View file

@ -28,14 +28,3 @@ data class NewsRelatedTokenDto(
@Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String,
)
@JsonClass(generateAdapter = true)
data class NewsOriginalArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "sourceName") val sourceName: String,
@Json(name = "language") val language: String,
@Json(name = "publishedAt") val publishedAt: String,
@Json(name = "url") val url: String,
@Json(name = "imageUrl") val imageUrl: String? = null,
)

View file

@ -16,5 +16,22 @@ data class NewsDetailsResponse(
@Json(name = "newsUrl") val newsUrl: String,
@Json(name = "shortContent") val shortContent: String,
@Json(name = "content") val content: String,
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
@Json(name = "relatedArticles") val relatedArticles: List<NewsRelatedArticleDto>,
)
@JsonClass(generateAdapter = true)
data class NewsRelatedArticleDto(
@Json(name = "id") val id: Int,
@Json(name = "title") val title: String,
@Json(name = "media") val media: Media,
@Json(name = "language") val language: String,
@Json(name = "publishedAt") val publishedAt: String,
@Json(name = "url") val url: String,
@Json(name = "imageUrl") val imageUrl: String? = null,
)
@JsonClass(generateAdapter = true)
data class Media(
@Json(name = "id") val id: Int,
@Json(name = "name") val name: String,
)

View file

@ -25,7 +25,6 @@ interface TangemPayApi {
@GET("v1/customer/wallets/{customer_wallet_id}")
suspend fun checkCustomerWalletId(
@Header("X-API-KEY") authHeader: String,
@Path("customer_wallet_id") customerWalletId: String,
): ApiResponse<CheckCustomerWalletResponse>

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