diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 5647ad7cfc..29b9efc572 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -75,6 +75,10 @@
+
+
+
+
diff --git a/Marathonfile b/Marathonfile
new file mode 100644
index 0000000000..91d15ba5fa
--- /dev/null
+++ b/Marathonfile
@@ -0,0 +1,41 @@
+name: "Tangem Android UI Tests"
+outputDir: "build/reports/marathon"
+
+testOutputTimeoutMillis: 180000 # 3 minutes
+testBatchTimeoutMillis: 600000 # 10 minutes
+
+vendorConfiguration:
+ type: "Android"
+ applicationApk: "app/build/outputs/apk/google/mocked/app-google-mocked.apk"
+ testApplicationApk: "app/build/outputs/apk/androidTest/google/mocked/app-google-mocked-androidTest.apk"
+ autoGrantPermission: true
+ applicationPmClear: true
+ testApplicationPmClear: true
+ # wiremockBaseUrl redirects API calls from wiremock.tests-d.com to local WireMock
+ # This is used on CI where each emulator has its own WireMock instance via adb reverse
+ instrumentationArgs:
+ wiremockBaseUrl: "http://localhost:8080"
+ allureConfiguration:
+ enabled: false # we collect allure via adb pull, not through marathon
+
+poolingStrategy:
+ type: "omni"
+
+batchingStrategy:
+ type: "isolate"
+
+sortingStrategy:
+ type: "no-sorting"
+
+retryStrategy:
+ type: "fixed-quota"
+ totalAllowedRetryQuota: 30
+ retryPerTestQuota: 1
+
+flakinessStrategy:
+ type: "ignore"
+
+analyticsConfiguration:
+ type: "disabled"
+
+debug: false
diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
index 8a40e56185..009af28e09 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt
@@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.printToLog
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
+import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.withForcedAllureSupport
@@ -19,6 +20,7 @@ import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.tap.MainActivity
@@ -86,6 +88,9 @@ abstract class BaseTestCase : TestCase(
additionalAfterSection: () -> Unit = {},
) = before {
Allure.label(ALLURE_LABEL_NAME, ALLURE_LABEL_VALUE)
+ // Setup WireMock redirect for CI with local WireMock instances
+ val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG)
+ WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl
hiltRule.inject()
runBlocking {
appPreferencesStore.editData { mutablePreferences ->
@@ -138,12 +143,16 @@ abstract class BaseTestCase : TestCase(
private fun applicationInjectionRule(): ApplicationInjectionExecutionRule {
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
)
)
}
+
+ private companion object {
+ const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt
index cb7ed9ff51..fd5d74d374 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt
@@ -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
diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt
index fe4c30d9a6..1095a7aa96 100644
--- a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt
+++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt
@@ -1,22 +1,31 @@
package com.tangem.common.utils
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.IOException
+private const val DEFAULT_WIREMOCK_URL = "[REDACTED_ENV_URL]"
+
+/**
+ * Returns the WireMock base URL to use.
+ */
+private fun getWireMockBaseUrl(): String =
+ WireMockRedirectInterceptor.overriddenBaseUrl ?: DEFAULT_WIREMOCK_URL
+
/**
* Method uses to set WireMock scenario state
* @param scenarioName Name of the scenario to modify
* @param state The target state to set (must be one of the scenario's possibleStates)
- * @param baseUrl WireMock base URL
+ * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
* @return true if state was set successfully, false otherwise
*/
fun setWireMockScenarioState(
scenarioName: String,
state: String,
- baseUrl: String = "[REDACTED_ENV_URL]"
+ baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Set ===")
Timber.i("Setting scenario '$scenarioName' to state: $state")
@@ -46,8 +55,9 @@ fun setWireMockScenarioState(
/**
* Method checks accessibility of WireMock
+ * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/
-fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
+fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean {
val client = OkHttpClient()
val request = Request.Builder()
.url("$baseUrl/__admin/scenarios")
@@ -69,8 +79,9 @@ fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
/**
* Method to reset all WireMock scenarios
+ * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
*/
-fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
+fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean {
Timber.i("=== WireMock Scenarios Reset ===")
Timber.i("Base URL: $baseUrl")
@@ -105,13 +116,13 @@ fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
* Method to reset a specific WireMock scenario to its initial state
* @param scenarioName Name of the scenario to reset
* @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates)
- * @param baseUrl WireMock base URL
+ * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote)
* @return true if reset was successful, false otherwise
*/
fun resetWireMockScenarioState(
scenarioName: String,
initialState: String = "Started",
- baseUrl: String = "[REDACTED_ENV_URL]"
+ baseUrl: String = getWireMockBaseUrl()
): Boolean {
Timber.i("=== WireMock Scenario Reset ===")
Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState")
diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt
index bca0130819..57d70b597e 100644
--- a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt
+++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt
@@ -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
@@ -76,4 +77,16 @@ fun BaseTestCase.checkActionIsUnavailableDialog() {
step("Assert 'Action is unavailable' dialog 'Ok' button is displayed") {
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() }
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt
index 6bcaa775de..066600df5e 100644
--- a/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt
+++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SendScenarios.kt
@@ -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() }
+ }
}
}
@@ -138,4 +142,29 @@ fun BaseTestCase.checkRecentAddressItem(address: String, description: String?) {
recentAddressItem(recipientAddress = address, description = description).assertIsDisplayed()
}
}
+}
+
+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) }
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DataNotLoadedDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DataNotLoadedDialogPageObject.kt
new file mode 100644
index 0000000000..e7efab995c
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/screens/DataNotLoadedDialogPageObject.kt
@@ -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(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)
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt
deleted file mode 100644
index 35a55722df..0000000000
--- a/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt
+++ /dev/null
@@ -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(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)
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt
index aa4b7b3647..0458682ee0 100644
--- a/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SendAddressPageObject.kt
@@ -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 {
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SendSelectNetworkFeeBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SendSelectNetworkFeeBottomSheetPageObject.kt
new file mode 100644
index 0000000000..977765ac50
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SendSelectNetworkFeeBottomSheetPageObject.kt
@@ -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(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)
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt
new file mode 100644
index 0000000000..6705194521
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt
@@ -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(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)
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt
index d2f4fa7b1b..188c9ece2e 100644
--- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt
+++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt
@@ -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
}
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt
index e4ac654ef2..fb132f5e1d 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt
@@ -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") {
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt
index d237730598..f57ae57734 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt
@@ -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 {
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt
index fd03d3bd1e..e807c84361 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt
@@ -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.*
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt
index a531a3cc1a..d14e14e295 100644
--- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt
+++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt
@@ -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") {
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt
new file mode 100644
index 0000000000..31b9035761
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt
@@ -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() }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ebc857a78a..97a76b6ac8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -307,6 +307,17 @@
android:host="tangem.onelink.me"
android:scheme="https" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt
index bf7f5f336a..6b59b1b8b2 100644
--- a/app/src/main/java/com/tangem/tap/TangemApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt
@@ -42,6 +42,7 @@ import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@@ -334,15 +335,23 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
ExceptionHandler.append(blockchainExceptionHandler)
if (LogConfig.network.isBlockchainSdkNetworkLogEnabled) {
- BlockchainSdkRetrofitBuilder.interceptors = listOf(
- createNetworkLoggingInterceptor(),
- ChuckerInterceptor(this),
- )
+ BlockchainSdkRetrofitBuilder.interceptors = buildList {
+ if (BuildConfig.MOCK_DATA_SOURCE) {
+ add(WireMockRedirectInterceptor())
+ }
+ add(createNetworkLoggingInterceptor())
+ add(ChuckerInterceptor(this@TangemApplication))
+ }
TangemApiServiceSettings.addInterceptors(
- createNetworkLoggingInterceptor(),
- ChuckerInterceptor(this),
- NetworkLogsSaveInterceptor(appLogsStore),
+ *buildList {
+ if (BuildConfig.MOCK_DATA_SOURCE) {
+ add(WireMockRedirectInterceptor())
+ }
+ add(createNetworkLoggingInterceptor())
+ add(ChuckerInterceptor(this@TangemApplication))
+ add(NetworkLogsSaveInterceptor(appLogsStore))
+ }.toTypedArray(),
)
}
@@ -360,7 +369,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private fun createReduxStore(): Store {
return Store(
- reducer = { action, state -> appReducer(action, state) },
+ reducer = { action, state -> appReducer(action, requireNotNull(state)) },
middleware = AppState.getMiddleware(),
state = AppState(
daggerGraphState = DaggerGraphState(
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt
index ed1ca3be0a..68a63f0c00 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt
@@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLink
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.wallets.models.AppsFlyerConversionData
+import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -18,6 +19,7 @@ import kotlin.contracts.contract
@Singleton
class AppsFlyerReferralParamsHandler @Inject constructor(
private val appsFlyerStore: AppsFlyerStore,
+ private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
dispatchers: CoroutineDispatcherProvider,
) {
@@ -69,6 +71,8 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
private fun storeConversionData(refcode: String, campaign: String?) {
coroutineScope.launch {
mutex.withLock {
+ setShouldShowMobileWalletPromoUseCase()
+ .onLeft { Timber.e(it) }
appsFlyerStore.storeIfAbsent(
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
)
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
index 71fcd29497..65315fc4cd 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt
@@ -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(
diff --git a/app/src/main/java/com/tangem/tap/core/ui/DefaultDesignFeatureToggles.kt b/app/src/main/java/com/tangem/tap/core/ui/DefaultDesignFeatureToggles.kt
new file mode 100644
index 0000000000..ca241efce8
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/core/ui/DefaultDesignFeatureToggles.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt
index de5f7ef9e3..b5b3c5ea29 100644
--- a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt
@@ -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
}
}
diff --git a/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt b/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt
new file mode 100644
index 0000000000..b02028c5bf
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/di/core/ui/CoreUiBindsModule.kt
@@ -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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
index 6e154f3fc1..e56953ffdc 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
@@ -115,7 +115,7 @@ object MarketsDomainModule {
return FilterAvailableNetworksForWalletUseCase(
userWalletsListManager = userWalletsListManager,
userWalletsListRepository = userWalletsListRepository,
- useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
+ shouldUseNewRepository = hotWalletFeatureToggles.isHotWalletEnabled,
excludedBlockchains = excludedBlockchains,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt
index 2fbc11170b..cf1a7707c6 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt
@@ -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 {
diff --git a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt
index 55b1457a55..d3e9da60e7 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt
@@ -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)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt
index 5e28ca4628..256d103aa3 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt
@@ -83,4 +83,14 @@ internal object NotificationsDomainModule {
): GetNetworksAvailableForNotificationsUseCase {
return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository)
}
+
+ @Provides
+ @Singleton
+ fun provideClearApplicationIdUseCase(
+ pushNotificationsRepository: PushNotificationsRepository,
+ ): ClearApplicationIdUseCase {
+ return ClearApplicationIdUseCase(
+ pushNotificationsRepository = pushNotificationsRepository,
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt
index 08a4bc7a3b..a823079f33 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt
@@ -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,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
index 7528f0f18b..5bb200ad04 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt
@@ -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
diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt
index 5ce513772f..55dab8c3b9 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt
@@ -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 {
@@ -266,6 +272,128 @@ internal object TransactionDomainModule {
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,
+ )
+ }
+
@Provides
@Singleton
fun provideSignCloreMessageUseCase(
diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
index ba29372fdb..53c7fa8abd 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
@@ -336,10 +336,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,
)
}
diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt
index 3301b9c21a..c893ae8a08 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt
@@ -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")
@@ -201,6 +204,16 @@ internal object YieldSupplyDomainModule {
)
}
+ @Provides
+ @Singleton
+ fun provideYieldSupplyEnterStatusFlowUseCase(
+ yieldSupplyRepository: YieldSupplyRepository,
+ ): YieldSupplyEnterStatusFlowUseCase {
+ return YieldSupplyEnterStatusFlowUseCase(
+ yieldSupplyRepository = yieldSupplyRepository,
+ )
+ }
+
@Provides
@Singleton
fun provideYieldSupplyGetShouldShowMainPromoUseCase(
@@ -234,4 +247,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),
+ )
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt
index 3d66410898..382b50ad26 100644
--- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt
+++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt
@@ -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(
diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
index 9ea074747f..6768c39ec9 100644
--- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt
@@ -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 ->
diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt
index 597915ff66..79a50c1403 100644
--- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt
+++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt
@@ -59,6 +59,17 @@ internal class DefaultAuthProvider(
}
}
+ override fun getGaslessServiceApiKey(apiEnvironment: Provider): ProviderSuspend {
+ 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 {
return if (shouldUseNewListRepository) {
userWalletsListRepository.userWalletsSync()
diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt
index 5f54236082..93b0595647 100644
--- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt
+++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt
@@ -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
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt
deleted file mode 100644
index f8350e77a9..0000000000
--- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.tangem.tap.proxy
-
-import com.tangem.blockchain.common.AmountType
-import com.tangem.blockchain.common.Blockchain
-import com.tangem.blockchain.common.WalletManager
-import com.tangem.blockchainsdk.utils.fromNetworkId
-import com.tangem.domain.walletmanager.WalletManagersFacade
-import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
-import com.tangem.lib.crypto.UserWalletManager
-import com.tangem.lib.crypto.models.ProxyAmount
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import kotlinx.coroutines.withContext
-import timber.log.Timber
-import java.math.BigDecimal
-
-class UserWalletManagerImpl(
- private val walletManagersFacade: WalletManagersFacade,
- private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
- private val dispatchers: CoroutineDispatcherProvider,
-) : UserWalletManager {
-
- override fun getWalletId(): String {
- val selectedUserWallet = requireNotNull(
- getSelectedWalletUseCase.sync().getOrNull(),
- ) { "selectedUserWallet shouldn't be null" }
- return selectedUserWallet.walletId.stringValue
- }
-
- override suspend fun hideAllTokens() {
- // FIXME: Used only in Tester Actions
- Timber.w("Not implemented")
- }
-
- override suspend fun getWalletAddress(networkId: String, derivationPath: String?): String {
- val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
- val walletManager = getActualWalletManager(blockchain, derivationPath)
- return walletManager.wallet.address
- }
-
- override suspend fun getLastTransactionHash(networkId: String, derivationPath: String?): String? {
- val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
- val walletManager = getActualWalletManager(blockchain, derivationPath)
- return walletManager.wallet.recentTransactions
- .lastOrNull { it.hash?.isNotEmpty() == true }
- ?.hash
- }
-
- override suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? {
- val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
- val walletManager = getActualWalletManager(blockchain, derivationPath)
- return walletManager.wallet.amounts.firstNotNullOfOrNull { amountEntry ->
- amountEntry.takeIf { amountEntry.key is AmountType.Coin }
- }?.value?.let { amount ->
- ProxyAmount(
- amount.currencySymbol,
- amount.value ?: BigDecimal.ZERO,
- amount.decimals,
- )
- }
- }
-
- @Throws(IllegalArgumentException::class)
- private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
- val selectedUserWallet = requireNotNull(
- getSelectedWalletUseCase.sync().getOrNull(),
- ) { "userWallet or userWalletsListManager is null" }
- val walletManager = withContext(dispatchers.io) {
- walletManagersFacade.getOrCreateWalletManager(
- selectedUserWallet.walletId,
- blockchain,
- derivationPath,
- )
- }
-
- return requireNotNull(walletManager) {
- "No wallet manager found"
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt
index 677d7e5ebd..5c943d917b 100644
--- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt
@@ -1,11 +1,6 @@
package com.tangem.tap.proxy.di
-import com.tangem.domain.walletmanager.WalletManagersFacade
-import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
-import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
-import com.tangem.tap.proxy.UserWalletManagerImpl
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -21,18 +16,4 @@ internal object ProxyModule {
fun provideAppStateHolder(): AppStateHolder {
return AppStateHolder()
}
-
- @Provides
- @Singleton
- fun provideUserWalletManager(
- walletManagersFacade: WalletManagersFacade,
- getSelectedWalletUseCase: GetSelectedWalletUseCase,
- dispatchers: CoroutineDispatcherProvider,
- ): UserWalletManager {
- return UserWalletManagerImpl(
- walletManagersFacade = walletManagersFacade,
- getSelectedWalletUseCase = getSelectedWalletUseCase,
- dispatchers = dispatchers,
- )
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
index d7f3a0cc33..95d4585b71 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
@@ -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")
@@ -147,6 +147,7 @@ internal class ChildFactory @Inject constructor(
val source = when (route.source) {
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
+ AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
}
val mode = when (val portfolio = route.portfolioId) {
@@ -205,21 +206,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 +331,7 @@ internal class ChildFactory @Inject constructor(
params = StakingComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.cryptoCurrency,
- yieldId = route.yieldId,
+ integrationId = route.integrationId,
),
componentFactory = stakingComponentFactory,
)
@@ -673,9 +692,7 @@ internal class ChildFactory @Inject constructor(
deeplink = mode.deeplink,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings
- is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain(
- userWalletId = mode.userWalletId,
- )
+ is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain
},
componentFactory = tangemPayOnboardingComponentFactory,
)
@@ -687,25 +704,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,
)
}
}
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
index 097b57b518..27c065e869 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt
@@ -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
+ }
+ }
}
}
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index d91dbb1dbe..6ec92245ac 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -16,7 +16,7 @@
diff --git a/app/src/mocked/res/xml/network_security_config.xml b/app/src/mocked/res/xml/network_security_config.xml
index 52c44ac992..a4929411f1 100644
--- a/app/src/mocked/res/xml/network_security_config.xml
+++ b/app/src/mocked/res/xml/network_security_config.xml
@@ -6,4 +6,9 @@
+
+
+ 10.0.2.2
+ localhost
+
diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt
index 5b7dfd4403..c804791992 100644
--- a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt
@@ -3,9 +3,12 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLink
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.domain.wallets.models.AppsFlyerConversionData
+import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.test.core.ProvideTestModels
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
+import arrow.core.right
import io.mockk.clearMocks
+import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
@@ -22,9 +25,13 @@ import org.junit.jupiter.params.ParameterizedTest
class AppsFlyerReferralParamsHandlerTest {
private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true)
+ private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase = mockk {
+ coEvery { this@mockk.invoke() } returns Unit.right()
+ }
private val handler = AppsFlyerReferralParamsHandler(
appsFlyerStore = appsFlyerStore,
dispatchers = TestingCoroutineDispatcherProvider(),
+ setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
)
@AfterEach
@@ -43,6 +50,7 @@ class AppsFlyerReferralParamsHandlerTest {
if (model.shouldStore) {
val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN)
+
coVerify { appsFlyerStore.storeIfAbsent(value = value) }
} else {
coVerify(inverse = true) { appsFlyerStore.storeIfAbsent(value = any()) }
diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
index ea548398d0..19782fca91 100644
--- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt
@@ -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(relaxed = true) {
every { sdk.uiVisibility() } returns MutableStateFlow(false)
}
+
+ private val newsDeeplink = mockk(relaxed = true) {
+ every { create(any(), any()) } returns mockk()
+ }
+ private val feedFeatureToggle = mockk()
+
private val mockedUri = mockk(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)
diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts
index 9fe3f2a5e4..b34c827c74 100644
--- a/common/routing/build.gradle.kts
+++ b/common/routing/build.gradle.kts
@@ -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)
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
index 7242c08646..a514a89100 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
@@ -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
@@ -137,6 +138,7 @@ sealed class AppRoute(val path: String) : Route {
enum class Source {
STORIES,
SETTINGS,
+ ACCOUNT,
}
}
@@ -209,8 +211,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(
@@ -443,9 +445,7 @@ sealed class AppRoute(val path: String) : Route {
) : Mode()
@Serializable
- data class FromBannerOnMain(
- val userWalletId: UserWalletId,
- ) : Mode()
+ data object FromBannerOnMain : Mode()
@Serializable
data object FromBannerInSettings : Mode()
@@ -456,16 +456,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")
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt
index 0319e1c59f..1ba2efe988 100644
--- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt
+++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt
@@ -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"
+ }
}
}
\ No newline at end of file
diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt
index 1e814c5d9f..4dc0b16d03 100644
--- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt
+++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockP2PEthPoolAccountResponseFactory.kt
@@ -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(),
),
)
diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt
index f2931ff828..3215446426 100644
--- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt
+++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt
@@ -181,7 +181,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
.alignByBaseline()
.heightIn(min = TangemTheme.dimens.size16)
.background(
- color = TangemTheme.colors.field.primary,
+ color = TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersSmall2,
)
.padding(horizontal = TangemTheme.dimens.spacing5),
diff --git a/common/ui/detekt-baseline-debug.xml b/common/ui/detekt-baseline-debug.xml
index 13ac4c3486..312ce47852 100644
--- a/common/ui/detekt-baseline-debug.xml
+++ b/common/ui/detekt-baseline-debug.xml
@@ -9,20 +9,11 @@
BooleanPropertyNaming:NotificationUM.kt$NotificationUM.Error.ExceedsBalance$val mergeFeeNetworkName: Boolean = false
BooleanPropertyNaming:NotificationsFactory.kt$NotificationsFactory$val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero()
BooleanPropertyNaming:TokenReceiveBottomSheetConfig.kt$TokenReceiveBottomSheetConfig$val showMemoDisclaimer: Boolean
- CanBeNonNullable:AmountBlockV2.kt$onClick: (() -> Unit)? = null
- CanBeNonNullable:NavigationButtonsBlock.kt$footerText: TextReference?
- CanBeNonNullable:NavigationButtonsBlock.kt$pairButtons: Pair<NavigationButton, NavigationButton>?
- CanBeNonNullable:NavigationButtonsBlock.kt$prevButton: NavigationButton?
- CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$dustValue: BigDecimal?
- CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minAdaValue: BigDecimal?
- CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$minimumSendAmount: BigDecimal?
- CanBeNonNullable:NotificationsFactory.kt$NotificationsFactory$rentWarning: CryptoCurrencyWarning.Rent?
MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -> { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -> { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -> 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(), ) }
MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) }
NoNameShadowing:NavigationButtonsBlock.kt$navigationUM
NoNameShadowing:UserWalletItem.kt$balance
NullableBooleanCheck:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$cryptoCurrencyStatus.value.yieldSupplyStatus?.isActive ?: false
- NullableToStringCall:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$${id.rawCurrencyId}
ReusedModifierInstance:AddTokenContent.kt$AddButton( modifier = modifier.fillMaxWidth(), state = state.button, )
UnnecessaryEventHandlerParameter:SendDoneButtons.kt$onShareClick: (String) -> Unit
UnnecessaryLet:TokenItemStateConverter.kt$TokenItemStateConverter.Companion$let(::add)
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt
index c24dd49e89..943fb98735 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt
@@ -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)
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt
index 188fcb1709..ba746abcc9 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt
@@ -6,6 +6,7 @@ import com.tangem.common.ui.R
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
+import com.tangem.core.ui.components.account.PaymentAccountIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
@@ -16,7 +17,7 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
* - a single character (derived from the [name]) using [AccountCharIcon], or
* - a predefined vector resource (from [icon]) using [AccountResIcon].
*
- * The background color is taken from [CryptoPortfolioIconUM.color],
+ * The background color is taken from [AccountIconUM.CryptoPortfolio.color],
* and the icon size, text style, and container shape are adapted based on the given [size].
*
* @param name A [TextReference] used to resolve and display the first letter
@@ -31,29 +32,43 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
@Composable
fun AccountIcon(
name: TextReference,
- icon: CryptoPortfolioIconUM,
+ icon: AccountIconUM.CryptoPortfolio,
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val letter = name.resolveReference().firstOrNull()
+ val iconColor = icon.color.getUiColor()
when {
icon.value == CryptoPortfolioIcon.Icon.Letter && letter == null -> {
AccountResIcon(
resId = R.drawable.ic_tangem_24,
- color = icon.color.getUiColor(),
+ color = iconColor,
size = size,
modifier = modifier,
)
}
icon.value == CryptoPortfolioIcon.Icon.Letter -> AccountCharIcon(
char = letter ?: 'N',
- color = icon.color.getUiColor(),
+ color = iconColor,
size = size,
modifier = modifier,
)
else -> AccountResIcon(
resId = icon.value.getResId(),
- color = icon.color.getUiColor(),
+ color = iconColor,
+ size = size,
+ modifier = modifier,
+ )
+ }
+}
+
+@Composable
+fun AccountIcon(name: TextReference, icon: AccountIconUM, size: AccountIconSize, modifier: Modifier = Modifier) {
+ when (icon) {
+ is AccountIconUM.Payment -> PaymentAccountIcon(size = size, modifier = modifier)
+ is AccountIconUM.CryptoPortfolio -> AccountIcon(
+ name = name,
+ icon = icon,
size = size,
modifier = modifier,
)
@@ -62,7 +77,7 @@ fun AccountIcon(
object AccountIconPreviewData {
- fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM(
+ fun randomAccountIcon(letter: Boolean = false) = AccountIconUM.CryptoPortfolio(
value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(),
color = Color.entries.random(),
)
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt
index f89a37e3db..102be5a837 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt
@@ -20,5 +20,6 @@ object AccountIconItemStateConverter : Converter TODO("[REDACTED_JIRA]")
}
}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconUM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconUM.kt
new file mode 100644
index 0000000000..ca79e78402
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconUM.kt
@@ -0,0 +1,10 @@
+package com.tangem.common.ui.account
+
+import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
+import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
+
+sealed class AccountIconUM {
+ data class CryptoPortfolio(val value: Icon, val color: Color) : AccountIconUM()
+
+ data object Payment : AccountIconUM()
+}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt
index 51f13b848f..4938e46e43 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt
@@ -27,7 +27,7 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun AccountLabel(
name: TextReference,
- icon: CryptoPortfolioIconUM,
+ icon: AccountIconUM,
iconSize: AccountIconSize,
modifier: Modifier = Modifier,
nameStyle: TextStyle = TangemTheme.typography.subtitle2,
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt
index d45ef488d1..6bb24f24e3 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt
@@ -22,21 +22,24 @@ class AccountPortfolioItemUMConverter(
) : Converter {
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,
@@ -45,13 +48,15 @@ class AccountPortfolioItemUMConverter(
)
UserWalletItemUM.Information.Loaded(text)
}
+ is Account.Payment -> TODO("[REDACTED_JIRA]")
}
- 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(),
+ icon = CryptoPortfolioIconConverter.convert(account.icon),
)
+ is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getBalanceInfo(): UserWalletItemUM.Balance {
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt
index 0b51e38465..8e755eb8bc 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt
@@ -41,7 +41,7 @@ import com.tangem.core.ui.res.TangemThemePreview
fun AccountRow(
title: TextReference,
subtitle: TextReference,
- icon: CryptoPortfolioIconUM,
+ icon: AccountIconUM.CryptoPortfolio,
modifier: Modifier = Modifier,
isReverse: Boolean = false,
) {
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt
index d21a35276b..a77848f899 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitle.kt
@@ -1,7 +1,9 @@
package com.tangem.common.ui.account
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -9,11 +11,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendScreenTestTags
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.utils.StringsSigns
/**
* A composable function that displays an account label (icon + name) with an optional prefix.
@@ -61,4 +68,28 @@ fun AccountTitle(
)
}
}
+}
+
+@Preview
+@Composable
+private fun PreviewAccountTitle() {
+ TangemThemePreview {
+ Column {
+ AccountTitle(
+ accountTitleUM = AccountTitleUM.Account(
+ prefixText = stringReference(StringsSigns.DOT),
+ name = stringReference("Main Wallet"),
+ icon = AccountIconUM.CryptoPortfolio(
+ value = CryptoPortfolioIcon.Icon.Bookmark,
+ color = CryptoPortfolioIcon.Color.Pattypan,
+ ),
+ ),
+ modifier = Modifier.padding(4.dp),
+ )
+ AccountTitle(
+ accountTitleUM = AccountTitleUM.Account.payment(prefixText = stringReference(StringsSigns.DOT)),
+ modifier = Modifier.padding(4.dp),
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitleUM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitleUM.kt
index 5b9537de75..f5cdef91d2 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitleUM.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountTitleUM.kt
@@ -1,7 +1,9 @@
package com.tangem.common.ui.account
import androidx.compose.runtime.Immutable
+import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.resourceReference
/**
* A sealed interface representing the title of an account, which can be either a simple text
@@ -19,6 +21,16 @@ sealed interface AccountTitleUM {
data class Account(
val prefixText: TextReference,
val name: TextReference,
- val icon: CryptoPortfolioIconUM,
- ) : AccountTitleUM
+ val icon: AccountIconUM,
+ ) : AccountTitleUM {
+ companion object {
+ fun payment(prefixText: TextReference = TextReference.EMPTY): Account {
+ return Account(
+ prefixText = prefixText,
+ name = resourceReference(R.string.tangempay_payment_account),
+ icon = AccountIconUM.Payment,
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconConverter.kt
new file mode 100644
index 0000000000..cfb11a33be
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconConverter.kt
@@ -0,0 +1,14 @@
+package com.tangem.common.ui.account
+
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.utils.converter.TwoWayConverter
+
+object CryptoPortfolioIconConverter : TwoWayConverter {
+ override fun convert(value: CryptoPortfolioIcon): AccountIconUM.CryptoPortfolio {
+ return AccountIconUM.CryptoPortfolio(value = value.value, color = value.color)
+ }
+
+ override fun convertBack(value: AccountIconUM.CryptoPortfolio): CryptoPortfolioIcon {
+ return CryptoPortfolioIcon.ofCustomAccount(value = value.value, color = value.color)
+ }
+}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt
index d97401c0a3..14015c86b2 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt
@@ -47,7 +47,4 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int {
CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24
CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24
}
-}
-
-fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this)
-fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color)
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt
deleted file mode 100644
index cb8b2d22af..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.tangem.common.ui.account
-
-import com.tangem.domain.models.account.CryptoPortfolioIcon
-import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
-import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
-
-data class CryptoPortfolioIconUM(
- val value: Icon,
- val color: Color,
-) {
- constructor(domainModel: CryptoPortfolioIcon) : this(
- value = domainModel.value,
- color = domainModel.color,
- )
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt
index caa4ec4ce0..e4575a197c 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt
@@ -82,7 +82,7 @@ fun PortfolioSelectRow(
@Immutable
data class PortfolioSelectUM(
- val icon: CryptoPortfolioIconUM?,
+ val icon: AccountIconUM.CryptoPortfolio?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,
diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt
index b859d383d1..ea09230bd7 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt
@@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.account.AccountTitleUM
+import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
@@ -15,7 +16,7 @@ class AmountAccountConverter(
return if (value != null && isAccountsMode) {
AccountTitleUM.Account(
name = value.accountName.toUM().value,
- icon = value.icon.toUM(),
+ icon = CryptoPortfolioIconConverter.convert(value.icon),
prefixText = prefixText,
)
} else {
diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt
index d5eafdf94d..d5a6605ccd 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt
@@ -5,7 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitleUM
-import com.tangem.common.ui.account.toUM
+import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
@@ -88,7 +88,7 @@ object AmountStatePreviewData {
val amountStateV2Accounts = amountState.copy(
accountTitleUM = AccountTitleUM.Account(
name = AccountNameUM.DefaultMain.value,
- icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
+ icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
prefixText = resourceReference(R.string.common_from),
),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"),
diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt
index 137328bcf0..fcd6c07b50 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt
@@ -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()
diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt
index 3ffd401d66..6adccd58a0 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt
@@ -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()
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt
index 4054fddac7..2bb9af138c 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt
@@ -1,15 +1,23 @@
package com.tangem.common.ui.bottomsheet.chooseaddress
+import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SimpleSettingsRow
-import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
+import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
+import kotlinx.collections.immutable.persistentListOf
@Composable
fun ChooseAddressBottomSheet(config: TangemBottomSheetConfig) {
@@ -31,4 +39,40 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon
)
}
}
-}
\ No newline at end of file
+}
+
+// region Preview
+@Composable
+@Preview(showBackground = true, widthDp = 360)
+@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
+private fun ChooseAddressBottomSheet_Preview(
+ @PreviewParameter(ChooseAddressBottomSheetPreviewProvider::class) params: ChooseAddressBottomSheetConfig,
+) {
+ TangemThemePreview {
+ ChooseAddressBottomSheetContent(params)
+ }
+}
+
+private class ChooseAddressBottomSheetPreviewProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ ChooseAddressBottomSheetConfig(
+ addressModels = persistentListOf(
+ AddressModel(
+ displayName = stringReference("Main address"),
+ fullName = stringReference("Bitcoin"),
+ value = "0x1234...abcd",
+ type = AddressModel.Type.Default,
+ ),
+ AddressModel(
+ displayName = stringReference("Legacy address"),
+ fullName = stringReference("Bitcoin"),
+ value = "0x1234...abcd",
+ type = AddressModel.Type.Default,
+ ),
+ ),
+ onClick = {},
+ ),
+ )
+}
+// endregion
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt
index 70a3642e18..a02e20a5c1 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt
@@ -1,10 +1,9 @@
package com.tangem.common.ui.bottomsheet.chooseaddress
import com.tangem.common.ui.bottomsheet.receive.AddressModel
-import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
-import com.tangem.domain.models.network.Network
+import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -14,13 +13,12 @@ class ChooseAddressBottomSheetConfig(
val onClick: (AddressModel) -> Unit,
) : TangemBottomSheetConfigContent {
constructor(
- asset: TokenReceiveBottomSheetConfig.Asset,
- network: Network,
+ currency: CryptoCurrency,
networkAddress: NetworkAddress,
onClick: (AddressModel) -> Unit,
) : this(
addressModels = networkAddress.availableAddresses
- .mapToAddressModels(asset, network)
+ .mapToAddressModels(cryptoCurrency = currency)
.toImmutableList(),
onClick = onClick,
)
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt
index f1c9f86b9d..2ad5ab5c2a 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt
@@ -1,9 +1,7 @@
package com.tangem.common.ui.bottomsheet.receive
-import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
-import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@@ -20,20 +18,6 @@ fun Set.mapToAddressModels(cryptoCurrency: CryptoCurrenc
cryptoCurrency.network,
)
-fun Set.mapToAddressModels(
- asset: TokenReceiveBottomSheetConfig.Asset,
- network: Network,
-): List = when (asset) {
- is TokenReceiveBottomSheetConfig.Asset.Currency -> mapToAddressModels(
- stringReference("${asset.name} (${asset.symbol})"),
- network,
- )
- is TokenReceiveBottomSheetConfig.Asset.NFT -> mapToAddressModels(
- resourceReference(R.string.common_nft),
- network,
- )
-}
-
private fun Set.mapToAddressModels(name: TextReference, network: Network): List =
this
.sortedBy { it.type.ordinal }
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt
deleted file mode 100644
index 9093a606aa..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt
+++ /dev/null
@@ -1,330 +0,0 @@
-package com.tangem.common.ui.bottomsheet.receive
-
-import android.content.res.Configuration
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastForEach
-import com.tangem.core.res.getStringSafe
-import com.tangem.core.ui.R
-import com.tangem.core.ui.components.SecondaryButtonIconStart
-import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
-import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
-import com.tangem.core.ui.components.notifications.Notification
-import com.tangem.core.ui.components.notifications.NotificationConfig
-import com.tangem.core.ui.components.rememberQrPainters
-import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
-import com.tangem.core.ui.extensions.*
-import com.tangem.core.ui.res.TangemTheme
-import com.tangem.core.ui.res.TangemThemePreview
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.coroutines.launch
-
-@Composable
-fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) {
- TangemBottomSheet(config) { content: TokenReceiveBottomSheetConfig ->
- TokenReceiveBottomSheetContent(content = content)
- }
-}
-
-@Composable
-private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) {
- var selectedAddress by remember { mutableStateOf(content.addresses.first()) }
-
- val snackbarHostState = remember(::SnackbarHostState)
-
- ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
- Column(
- modifier = Modifier
- .verticalScroll(state = rememberScrollState())
- .padding(top = 24.dp, bottom = 16.dp)
- .padding(horizontal = 24.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(space = 24.dp),
- ) {
- QrCodeContent(content = content, onAddressChange = { selectedAddress = it })
-
- Info(
- showMemoDisclaimer = content.showMemoDisclaimer,
- notifications = content.notifications,
- )
-
- Buttons(
- snackbarHostState = snackbarHostState,
- onShareClick = { content.onShareClick(selectedAddress.value) },
- onCopyClick = { content.onCopyClick(selectedAddress.value) },
- )
- }
- }
-}
-
-@Composable
-private fun Info(
- showMemoDisclaimer: Boolean,
- notifications: ImmutableList,
- modifier: Modifier = Modifier,
-) {
- Column(
- modifier = modifier,
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(space = 16.dp),
- ) {
- if (showMemoDisclaimer) {
- Text(
- modifier = Modifier
- .padding(horizontal = 18.dp)
- .fillMaxWidth(),
- text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
- style = TangemTheme.typography.caption1,
- color = TangemTheme.colors.text.tertiary,
- textAlign = TextAlign.Center,
- )
- }
-
- notifications.fastForEach { notificationConfig ->
- key(notificationConfig.hashCode()) {
- Notification(config = notificationConfig)
- }
- }
- }
-}
-
-@Composable
-private fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
- Box {
- content()
-
- CopiedTextSnackbarHost(
- hostState = snackbarHostState,
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .padding(bottom = 80.dp),
- )
- }
-}
-
-@Composable
-private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) {
- val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value))
-
- val pagerState = rememberPagerState(
- initialPage = 0,
- initialPageOffsetFraction = 0f,
- pageCount = content.addresses::count,
- )
-
- LaunchedEffect(key1 = pagerState.currentPage) {
- onAddressChange.invoke(content.addresses[pagerState.currentPage])
- }
-
- HorizontalPager(state = pagerState) { currentPage ->
- QrCodePage(
- content = content,
- qrCodePainter = qrCodes[currentPage],
- currentIndex = currentPage,
- )
- }
-
- if (pagerState.pageCount > 1) {
- val indicatorState = rememberLazyListState()
- val selectedColor = TangemTheme.colors.icon.primary1
- val unselectedColor = TangemTheme.colors.icon.informative
-
- LazyRow(
- modifier = Modifier.height(20.dp),
- state = indicatorState,
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- repeat(pagerState.pageCount) { iteration ->
- item(key = iteration) {
- val color by animateColorAsState(
- targetValue = if (pagerState.currentPage == iteration) selectedColor else unselectedColor,
- label = "",
- )
-
- Box(
- modifier = Modifier
- .padding(horizontal = 4.dp, vertical = 6.dp)
- .background(color = color, shape = CircleShape)
- .size(7.dp),
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Painter, currentIndex: Int) {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(24.dp),
- ) {
- Text(
- text = stringResourceSafe(
- R.string.receive_bottom_sheet_warning_message_compact,
- content.addresses[currentIndex].fullName.resolveReference(),
- content.network,
- ),
- color = TangemTheme.colors.text.primary1,
- textAlign = TextAlign.Center,
- style = TangemTheme.typography.h3,
- )
-
- Image(
- painter = qrCodePainter,
- contentDescription = null,
- contentScale = ContentScale.Fit,
- modifier = Modifier.size(248.dp),
- )
-
- Text(
- text = content.addresses[currentIndex].value,
- color = TangemTheme.colors.text.primary1,
- textAlign = TextAlign.Center,
- style = TangemTheme.typography.subtitle1,
- )
- }
-}
-
-@Composable
-private fun Buttons(
- snackbarHostState: SnackbarHostState,
- onShareClick: () -> Unit,
- onCopyClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- val hapticFeedback = LocalHapticFeedback.current
- val coroutineScope = rememberCoroutineScope()
- val context = LocalContext.current
- val resources = context.resources
-
- Row(
- modifier = modifier,
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- SecondaryButtonIconStart(
- modifier = Modifier.weight(1f),
- text = stringResourceSafe(id = R.string.common_copy),
- iconResId = R.drawable.ic_copy_24,
- onClick = {
- onCopyClick()
-
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-
- coroutineScope.launch {
- snackbarHostState.showSnackbar(
- message = resources.getStringSafe(R.string.wallet_notification_address_copied),
- )
- }
- },
- )
-
- SecondaryButtonIconStart(
- modifier = Modifier.weight(1f),
- text = stringResourceSafe(id = R.string.common_share),
- iconResId = R.drawable.ic_share_24,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
-
- onShareClick()
- },
- )
- }
-}
-
-// region Preview
-@Composable
-@Preview(showBackground = true, widthDp = 360)
-@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
-private fun Preview_TokenReceiveBottomSheet(
- @PreviewParameter(TokenReceiveBottomSheetConfigPreviewProvider::class) params: TokenReceiveBottomSheetConfig,
-) {
- TangemThemePreview {
- val config = TangemBottomSheetConfig(
- isShown = true,
- content = params,
- onDismissRequest = {},
- )
-
- TokenReceiveBottomSheet(config)
- }
-}
-
-private class TokenReceiveBottomSheetConfigPreviewProvider : PreviewParameterProvider {
- val address = AddressModel(
- displayName = stringReference("Address 1"),
- fullName = combinedReference(
- stringReference("Address 1"),
- stringReference(" Stellar"),
- ),
- value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
- type = AddressModel.Type.Default,
- )
- private val baseConfig = TokenReceiveBottomSheetConfig(
- asset = TokenReceiveBottomSheetConfig.Asset.Currency(
- name = "Stellar",
- symbol = "XLM",
- ),
- network = "Ethereum",
- notifications = persistentListOf(
- NotificationConfig(
- title = stringReference("Send only XLM on the Ethereum network"),
- subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
- iconResId = R.drawable.ic_alert_circle_24,
- ),
- ),
- addresses = persistentListOf(address),
- showMemoDisclaimer = false,
- onCopyClick = {},
- onShareClick = {},
- )
-
- override val values: Sequence
- get() = sequenceOf(
- baseConfig,
- baseConfig.copy(
- addresses = persistentListOf(
- address,
- address.copy(displayName = stringReference("Address 2")),
- ),
- ),
- baseConfig.copy(
- showMemoDisclaimer = true,
- ),
- baseConfig.copy(
- showMemoDisclaimer = true,
- addresses = persistentListOf(
- address,
- address.copy(displayName = stringReference("Address 2")),
- ),
- ),
- )
-}
-// endregion Preview
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt
deleted file mode 100644
index 5d29be17ec..0000000000
--- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.tangem.common.ui.bottomsheet.receive
-
-import androidx.compose.runtime.Immutable
-import com.tangem.core.ui.R
-import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
-import com.tangem.core.ui.components.notifications.NotificationConfig
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.stringReference
-import com.tangem.core.ui.extensions.wrappedList
-import com.tangem.domain.models.network.Network
-import com.tangem.domain.models.network.NetworkAddress
-import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.toImmutableList
-
-data class TokenReceiveBottomSheetConfig(
- val asset: Asset,
- val network: String,
- val addresses: ImmutableList,
- val showMemoDisclaimer: Boolean,
- val notifications: ImmutableList,
- val onCopyClick: (String) -> Unit,
- val onShareClick: (String) -> Unit,
-) : TangemBottomSheetConfigContent {
-
- constructor(
- asset: Asset,
- network: Network,
- networkAddress: NetworkAddress,
- showMemoDisclaimer: Boolean,
- customNotifications: ImmutableList = persistentListOf(),
- onCopyClick: (String) -> Unit,
- onShareClick: (String) -> Unit,
- ) : this(
- asset = asset,
- network = network.name,
- addresses = networkAddress.availableAddresses
- .mapToAddressModels(asset, network)
- .toImmutableList(),
- showMemoDisclaimer = showMemoDisclaimer,
- notifications = persistentListOf(defaultAssetNotificationConfig(asset, network)).addAll(0, customNotifications),
- onCopyClick = onCopyClick,
- onShareClick = onShareClick,
- )
-
- companion object {
- private fun defaultAssetNotificationConfig(asset: Asset, network: Network): NotificationConfig =
- NotificationConfig(
- title = resourceReference(
- R.string.receive_bottom_sheet_warning_title,
- wrappedList(asset.displaySymbol, network.name),
- ),
- subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
- iconResId = R.drawable.ic_alert_circle_24,
- iconTint = NotificationConfig.IconTint.Accent,
- )
- }
-
- @Immutable
- sealed class Asset {
- abstract val displaySymbol: TextReference
-
- data class Currency(val name: String, val symbol: String) : Asset() {
- override val displaySymbol = stringReference(symbol)
- }
-
- data object NFT : Asset() {
- override val displaySymbol = resourceReference(R.string.common_nft)
- }
- }
-}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt
new file mode 100644
index 0000000000..870d30bc07
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt
@@ -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,
+ 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,
+)
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt
index dde935dbec..639f70a3ec 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
+import com.tangem.core.ui.components.HoldToConfirmButton
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.buttons.common.TangemButton
@@ -98,34 +99,48 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier
modifier = modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
- val icon = if (button.iconRes != null && button.isIconVisible) {
- TangemButtonIconPosition.End(iconResId = button.iconRes)
- } else {
- TangemButtonIconPosition.None
- }
- val color = if (button.isDimmed) {
- TangemButtonsDefaults.secondaryButtonColors
- .copy(contentColor = TangemTheme.colors.text.tertiary)
- } else {
- TangemButtonsDefaults.primaryButtonColors
- }
- TangemButton(
- text = button.textReference.resolveReference(),
- enabled = button.isEnabled,
- onClick = {
- GlobalMultipleClickPreventer.processEvent {
- if (button.isHapticClick) {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+ if (button.isHoldToConfirm) {
+ HoldToConfirmButton(
+ text = button.textReference.resolveReference(),
+ enabled = button.isEnabled,
+ isLoading = button.shouldShowProgress,
+ onConfirm = {
+ GlobalMultipleClickPreventer.processEvent {
+ button.onClick()
}
- button.onClick()
- }
- },
- showProgress = button.shouldShowProgress,
- colors = color,
- textStyle = TangemTheme.typography.subtitle1,
- icon = icon,
- modifier = Modifier.fillMaxWidth(),
- )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ } else {
+ val icon = if (button.iconRes != null && button.isIconVisible) {
+ TangemButtonIconPosition.End(iconResId = button.iconRes)
+ } else {
+ TangemButtonIconPosition.None
+ }
+ val color = if (button.isDimmed) {
+ TangemButtonsDefaults.secondaryButtonColors
+ .copy(contentColor = TangemTheme.colors.text.tertiary)
+ } else {
+ TangemButtonsDefaults.primaryButtonColors
+ }
+ TangemButton(
+ text = button.textReference.resolveReference(),
+ enabled = button.isEnabled,
+ onClick = {
+ GlobalMultipleClickPreventer.processEvent {
+ if (button.isHapticClick) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+ }
+ button.onClick()
+ }
+ },
+ showProgress = button.shouldShowProgress,
+ colors = color,
+ textStyle = TangemTheme.typography.subtitle1,
+ icon = icon,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}
diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt
index 6191b4de9c..8b1bebbcd4 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt
@@ -34,5 +34,6 @@ data class NavigationButton(
val isEnabled: Boolean = true,
val isDimmed: Boolean = false,
val isHapticClick: Boolean = false,
+ val isHoldToConfirm: Boolean = false,
val onClick: () -> Unit,
)
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt
index 91e26bb895..a6a7a2a078 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt
@@ -1,33 +1,37 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
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.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+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
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
-import com.tangem.utils.StringsSigns
-import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
@@ -39,70 +43,137 @@ fun ArticleCard(
modifier: Modifier = Modifier,
colors: CardColors = TangemBlockCardColors,
) {
- BlockCard(
- modifier = modifier,
- onClick = onArticleClick,
- colors = colors,
+ 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,
+ onArticleClick: () -> Unit,
+ colors: CardColors,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .clip(TangemTheme.shapes.roundedCornersXMedium)
+ .background(colors.containerColor)
+ .clickable { onArticleClick() },
) {
- if (articleConfigUM.isTrending) {
- TrendingArticle(articleConfigUM = articleConfigUM)
- } else {
- DefaultArticle(articleConfigUM = articleConfigUM)
+ Image(
+ modifier = Modifier.matchParentSize(),
+ painter = painterResource(R.drawable.bg_trending_article),
+ contentDescription = null,
+ contentScale = ContentScale.FillWidth,
+ )
+ Column(
+ modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ modifier = Modifier
+ .background(
+ color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
+ shape = RoundedCornerShape(8.dp),
+ )
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ text = stringResourceSafe(R.string.feed_trending_now),
+ style = TangemTheme.typography.caption1,
+ color = TangemTheme.colors.icon.accent,
+ )
+
+ SpacerH(12.dp)
+
+ Text(
+ text = articleConfigUM.title,
+ color = if (articleConfigUM.isViewed) {
+ TangemTheme.colors.text.tertiary
+ } else {
+ TangemTheme.colors.text.primary1
+ },
+ style = TangemTheme.typography.h3,
+ textAlign = TextAlign.Center,
+ )
+
+ SpacerH(8.dp)
+
+ ArticleInfo(
+ score = articleConfigUM.score,
+ createdAt = articleConfigUM.createdAt.resolveReference(),
+ )
+
+ SpacerH(32.dp)
+
+ Tags(
+ modifier = Modifier.padding(horizontal = 46.dp),
+ tags = articleConfigUM.tags.toImmutableList(),
+ )
}
}
}
@Composable
-private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
- Column(
- modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
+fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
+ BlockCard(
+ modifier = modifier,
+ onClick = onClick,
+ colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
- Text(
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
- .background(
- color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
- shape = RoundedCornerShape(8.dp),
- )
- .padding(horizontal = 8.dp, vertical = 4.dp),
- text = stringResourceSafe(R.string.feed_trending_now),
- style = TangemTheme.typography.caption1,
- color = TangemTheme.colors.icon.accent,
- )
+ .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(12.dp)
+ SpacerH(16.dp)
- Text(
- text = articleConfigUM.title,
- color = if (articleConfigUM.isViewed) {
- TangemTheme.colors.text.tertiary
- } else {
- TangemTheme.colors.text.primary1
- },
- style = TangemTheme.typography.h3,
- maxLines = 3,
- overflow = TextOverflow.Ellipsis,
- )
+ Text(
+ text = stringResourceSafe(R.string.news_all_news),
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.primary1,
+ )
- SpacerH(8.dp)
-
- ArticleInfo(
- score = articleConfigUM.score,
- createdAt = articleConfigUM.createdAt.resolveReference(),
- )
-
- SpacerH(32.dp)
-
- Tags(
- modifier = Modifier.padding(horizontal = 46.dp),
- tags = articleConfigUM.tags.toImmutableList(),
- )
+ 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) {
- Column(modifier = Modifier.padding(12.dp)) {
+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(),
@@ -124,28 +195,9 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
SpacerH(16.dp)
- Tags(tags = articleConfigUM.tags.toImmutableList())
- }
-}
+ SpacerHMax()
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-private fun Tags(tags: ImmutableList, 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,
- modifier = modifier,
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- maxLines = 1,
- overflow = expandIndicator,
- ) { index ->
- Label(state = tags[index])
+ Tags(tags = articleConfigUM.tags.toImmutableList())
}
}
diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt
index f7cc656eae..a2834e2afb 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt
@@ -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,
)
}
diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt
index 4ef5f1ce75..300d41f104 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt
@@ -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)) {
diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt b/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt
new file mode 100644
index 0000000000..a163c8ee15
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt
@@ -0,0 +1,128 @@
+package com.tangem.common.ui.news
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.layout.SubcomposeMeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.label.Label
+import com.tangem.core.ui.components.label.entity.LabelUM
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.utils.StringsSigns
+import kotlinx.collections.immutable.ImmutableList
+
+private val TAG_SPACING = 4.dp
+
+/**
+ * A layout that displays a list of tags, automatically calculating how many can fit
+ * in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
+ *
+ * @param tags The immutable list of [LabelUM] to display.
+ * @param modifier The modifier to be applied to the layout.
+ */
+@Composable
+internal fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) {
+ if (tags.isEmpty()) return
+
+ SubcomposeLayout(modifier = modifier) { constraints ->
+ val spacingPx = TAG_SPACING.roundToPx()
+
+ val tagPlaceables = subcompose(ContentSlot.Tags) {
+ tags.forEach { tag ->
+ Label(state = tag)
+ }
+ }.map { it.measure(constraints) }
+
+ val layoutInfo = calculateLayoutInfo(
+ maxWidth = constraints.maxWidth,
+ spacingPx = spacingPx,
+ tagPlaceables = tagPlaceables,
+ )
+
+ val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
+ subcompose(ContentSlot.Overflow) {
+ OverflowLabel(count = it)
+ }.first().measure(constraints)
+ }
+
+ val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
+ val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
+
+ val width = calculateRowWidth(
+ placeables = allVisibleItems,
+ spacingPx = spacingPx,
+ ).coerceIn(constraints.minWidth, constraints.maxWidth)
+
+ val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
+
+ layout(width = width, height = height) {
+ var xPosition = 0
+ allVisibleItems.forEachIndexed { index, placeable ->
+ if (index > 0) {
+ xPosition += spacingPx
+ }
+ val yPosition = height - placeable.height
+ placeable.placeRelative(xPosition, yPosition)
+ xPosition += placeable.width
+ }
+ }
+ }
+}
+
+/**
+ * Calculates how many tags can be displayed and how many are overflowing.
+ */
+private fun SubcomposeMeasureScope.calculateLayoutInfo(
+ maxWidth: Int,
+ spacingPx: Int,
+ tagPlaceables: List,
+): TagsLayoutInfo {
+ val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
+
+ if (totalWidthWithoutOverflow <= maxWidth) {
+ return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
+ }
+
+ for (count in tagPlaceables.indices.reversed()) {
+ val visibleTags = tagPlaceables.take(count)
+ val overflowCount = tagPlaceables.size - count
+
+ val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
+ OverflowLabel(count = overflowCount)
+ }.first().measure(Constraints())
+
+ val widthWithOverflow = calculateRowWidth(
+ placeables = visibleTags + tempOverflowPlaceable,
+ spacingPx = spacingPx,
+ )
+
+ if (widthWithOverflow <= maxWidth) {
+ return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
+ }
+ }
+
+ return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
+}
+
+@Composable
+private fun OverflowLabel(count: Int) {
+ Label(
+ state = LabelUM(
+ text = TextReference.Str("${StringsSigns.PLUS}$count"),
+ maxLines = 1,
+ ),
+ )
+}
+
+private fun calculateRowWidth(placeables: List, spacingPx: Int): Int {
+ if (placeables.isEmpty()) return 0
+ val tagsWidth = placeables.sumOf { it.width }
+ val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
+ return tagsWidth + spacingWidth
+}
+
+private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
+
+private enum class ContentSlot { Tags, Overflow }
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt
index 668943b4c7..1cedf7d5b3 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt
@@ -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.addFeeUnreachableNotification(
@@ -314,12 +314,14 @@ object NotificationsFactory {
}
}
+ @Suppress("LongParameterList")
fun MutableList.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)
}
}
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt
similarity index 95%
rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt
rename to common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt
index 7df750dad1..8681f259a6 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt
@@ -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,
)
}
diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt
index 06cfd97a8a..6211063758 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt
@@ -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 = emptyMap(),
private val stakingApyMap: Map = 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,
- 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(
diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt
index b8f7104b03..f5643c66c3 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt
@@ -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 {
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
+ }
}
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt
index 3c59142510..75632fb64a 100644
--- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt
+++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt
@@ -1,6 +1,6 @@
package com.tangem.common.ui.userwallet.state
-import com.tangem.common.ui.account.CryptoPortfolioIconUM
+import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import javax.annotation.concurrent.Immutable
@@ -64,7 +64,7 @@ data class UserWalletItemUM(
data class Account(
val name: TextReference,
- val icon: CryptoPortfolioIconUM,
+ val icon: AccountIconUM.CryptoPortfolio,
) : ImageState()
data class Image(
diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
index 287b8a9170..083fa85275 100644
--- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
+++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
@@ -3,6 +3,8 @@ package com.tangem.core.analytics.models
import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL
import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL_ID
+const val IS_NOT_HTTP_ERROR = "Is not http error"
+
sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
@@ -80,7 +82,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")
@@ -90,6 +94,9 @@ sealed class AnalyticsParam {
data object AddNewWallet : ScreensSources("Add New Wallet")
data object AddNew : ScreensSources("Add New")
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) {
@@ -97,24 +104,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
@@ -122,6 +133,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")
@@ -130,24 +142,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?
}
@@ -271,6 +287,9 @@ sealed class AnalyticsParam {
const val ENS = "ENS"
const val ENS_ADDRESS = "ENS Address"
const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)"
+ const val ACCOUNT_DERIVATION_TO = "Account Derivation (to)"
+ const val FEE_TOKEN = "Fee Token"
+ const val ACCOUNT_DERIVATION = "Account Derivation"
const val REFERRAL = "Referral"
const val REFERRAL_ID = "Referral_ID"
}
diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt
index 7a67705605..3e71b47241 100644
--- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt
+++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt
@@ -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
diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt
new file mode 100644
index 0000000000..85f22ce2df
--- /dev/null
+++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt
@@ -0,0 +1,32 @@
+package com.tangem.core.analytics.models
+
+/**
+ * Marker interface for analytics events that can be throttled by session and/or time.
+ *
+ * Events implementing this interface will be tracked by their [oneTimeEventId].
+ *
+ * Behavior depends on [throttleSeconds]:
+ * - **null** (default): One time per session — event is sent only once during the application session.
+ * - **non-null**: Time-based throttling — event is not sent if less than [throttleSeconds] seconds
+ * have passed since the last send for this [oneTimeEventId].
+ *
+ * @see Analytics.send
+ */
+interface OneTimePerSessionEvent {
+ /**
+ * Unique identifier for the throttled event.
+ *
+ * This ID is used to track whether and when the event was last sent.
+ * Events with the same [oneTimeEventId] share the same throttling state.
+ */
+ val oneTimeEventId: String
+
+ /**
+ * Minimum interval in seconds between sends for this event.
+ *
+ * - **null**: One time per session only. Event is sent at most once per session.
+ * - **non-null**: Don't send if less than this many seconds have passed since the last send.
+ */
+ val throttleSeconds: Long?
+ get() = null
+}
\ No newline at end of file
diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt
index 9703947954..d6571cb78c 100644
--- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt
+++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt
@@ -6,12 +6,14 @@ 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
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
/**
[REDACTED_AUTHOR]
@@ -31,6 +33,7 @@ object Analytics : GlobalAnalyticsEventHandler {
private val handlers = mutableMapOf()
private val paramsInterceptors = ConcurrentHashMap()
+ private val throttledEventsState = ConcurrentHashMap()
private val analyticsFilters = mutableSetOf()
private val analyticsMutex = Mutex()
@@ -85,6 +88,9 @@ object Analytics : GlobalAnalyticsEventHandler {
override fun send(event: AnalyticsEvent) {
analyticsScope.launch {
+ if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) {
+ return@launch
+ }
event.params = applyParamsInterceptors(event)
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
@@ -130,6 +136,21 @@ object Analytics : GlobalAnalyticsEventHandler {
return interceptedParams
}
+ private fun shouldSendThrottledEvent(event: OneTimePerSessionEvent): Boolean {
+ val now = System.currentTimeMillis()
+ val id = event.oneTimeEventId
+ return when (val throttleMs = event.throttleSeconds?.let(TimeUnit.SECONDS::toMillis)) {
+ null -> throttledEventsState.putIfAbsent(id, now) == null
+ else -> throttledEventsState.compute(id) { _, lastSendTime ->
+ when {
+ lastSendTime == null -> now
+ now - lastSendTime >= throttleMs -> now
+ else -> lastSendTime
+ }
+ } == now
+ }
+ }
+
private fun createScope(): CoroutineScope {
val name = "Analytics"
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/ResettableOneTimeEventSender.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/ResettableOneTimeEventSender.kt
new file mode 100644
index 0000000000..3518dacfd8
--- /dev/null
+++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/ResettableOneTimeEventSender.kt
@@ -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()
+
+ fun sendEventOnce(key: String, event: AnalyticsEvent) {
+ if (sentEvents.putIfAbsent(key, event) == null) {
+ analyticsEventHandler.send(event)
+ }
+ }
+
+ fun reset(key: String) {
+ sentEvents.remove(key)
+ }
+}
\ No newline at end of file
diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
index abff08deb7..299c0abe09 100644
--- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
+++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json
@@ -47,28 +47,40 @@
"name": "TANGEM_PAY_ENABLED",
"version": "5.31.0"
},
- {
- "name": "TANGEM_PAY_ENTRYPOINT_ENABLED",
- "version": "undefined"
- },
- {
- "name": "NEW_TOKEN_RECEIVE_ENABLED",
- "version": "5.28.0"
- },
{
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
"version": "5.30.0"
},
+ {
+ "name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
+ "version": "5.33.0"
+ },
{
"name": "NEW_ONRAMP_MAIN_ENABLED",
"version": "5.31.0"
},
{
"name": "ACCOUNTS_FEATURE_ENABLED",
- "version": "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"
+ },
+ {
+ "name": "EARN_BLOCK_ENABLED",
"version": "undefined"
}
]
diff --git a/core/datasource/detekt-baseline-debug.xml b/core/datasource/detekt-baseline-debug.xml
index 7d14c576a1..08b13f32ec 100644
--- a/core/datasource/detekt-baseline-debug.xml
+++ b/core/datasource/detekt-baseline-debug.xml
@@ -12,6 +12,5 @@
NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withWriteTimeout(timeout = it.duration, unit = it.unit) }
UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$apiConfigs + (apiConfig to environment)
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")
- UnusedImports:NetworkModule.kt$import com.tangem.datasource.api.common.config.MoonPay
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt
index 002f8656ed..75cf5c12bd 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt
@@ -18,6 +18,8 @@ interface AuthProvider {
fun getApiKey(apiEnvironment: Provider): ProviderSuspend
+ fun getGaslessServiceApiKey(apiEnvironment: Provider): ProviderSuspend
+
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt
index 8e0a661871..515a93b2f8 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt
@@ -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
}
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/GaslessTxService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/GaslessTxService.kt
new file mode 100644
index 0000000000..35935f0c9d
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/GaslessTxService.kt
@@ -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 = 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]"
+ }
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/News.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/News.kt
index 23ff32ba22..fc57478ec5 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/News.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/News.kt
@@ -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 {
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt
index aa1b28c629..0dbc3bb4bb 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/P2PEthPool.kt
@@ -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
}
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt
index c0a5c07775..b494cbc70c 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt
@@ -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")
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt
deleted file mode 100644
index cbbc29459d..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPayAuth.kt
+++ /dev/null
@@ -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" },
- )
-}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt
index cf3ab2f3e5..f76e0a7bfa 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt
@@ -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>
+ @Body body: P2PEthPoolTransactionRequest,
+ ): ApiResponse>
/**
* 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>
+ @Body body: P2PEthPoolTransactionRequest,
+ ): ApiResponse>
/**
* 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>
+ @Body body: P2PEthPoolTransactionRequest,
+ ): ApiResponse>
/**
* Broadcast signed transaction
@@ -96,22 +94,4 @@ interface P2PEthPoolApi {
@Path("delegatorAddress") delegatorAddress: String,
@Path("vaultAddress") vaultAddress: String,
): ApiResponse>
-
- /**
- * 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>
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt
deleted file mode 100644
index e4b3200add..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolDepositRequest.kt
+++ /dev/null
@@ -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,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt
new file mode 100644
index 0000000000..500fc969be
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolTransactionRequest.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt
deleted file mode 100644
index 0e56cd5112..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolUnstakeRequest.kt
+++ /dev/null
@@ -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,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt
deleted file mode 100644
index d0a7aac5cf..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolWithdrawRequest.kt
+++ /dev/null
@@ -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,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt
index 526c3c7d16..647750c927 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountResponse.kt
@@ -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,
)
@@ -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,
)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt
index 20ec7b67f8..1970814c8c 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolBroadcastResponse.kt
@@ -29,7 +29,7 @@ data class P2PEthPoolBroadcastResponse(
)
/**
- * Transaction status from P2P API
+ * Transaction status from P2PEthPool API
*/
@JsonClass(generateAdapter = false)
enum class P2PEthPoolTxStatusDTO {
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt
deleted file mode 100644
index ed922bf827..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolDepositResponse.kt
+++ /dev/null
@@ -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,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt
index 88235092a1..c99f726d91 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt
@@ -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(
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolResponse.kt
index e9fe22fd51..d871ebbea8 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolResponse.kt
@@ -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 },
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolRewardsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolRewardsResponse.kt
deleted file mode 100644
index ef4dd03ff0..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolRewardsResponse.kt
+++ /dev/null
@@ -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,
-)
-
-@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,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolWithdrawResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolTransactionResponse.kt
similarity index 57%
rename from core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolWithdrawResponse.kt
rename to core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolTransactionResponse.kt
index 1d5ff5bb4a..9c44476b5c 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolWithdrawResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolTransactionResponse.kt
@@ -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,
+ val tickets: List? = null,
)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt
deleted file mode 100644
index a3e2391339..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolUnstakeResponse.kt
+++ /dev/null
@@ -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,
-)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolVaultsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolVaultsResponse.kt
index 5c825c6346..8a8eaf6167 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolVaultsResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolVaultsResponse.kt
@@ -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")
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt
index 2ef036c212..4712dadadc 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt
@@ -32,6 +32,9 @@ data class ExchangeProvider(
@Json(name = "slippage")
val slippage: BigDecimal?,
+
+ @Json(name = "exchangeOnlyWithinSingleAddress")
+ val isExchangeOnlyWithinSingleAddress: Boolean = false,
)
@JsonClass(generateAdapter = false)
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt
new file mode 100644
index 0000000000..514dc9f20e
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/GaslessTxServiceApi.kt
@@ -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>
+
+ @POST("api/v1/transaction/sign")
+ suspend fun signGaslessTransaction(
+ @Body transaction: GaslessTransactionRequest,
+ ): ApiResponse>
+
+ @GET("api/v1/config/fee-recipient")
+ suspend fun getFeeRecipient(): ApiResponse>
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessFeeRecipient.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessFeeRecipient.kt
new file mode 100644
index 0000000000..f66c161faf
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessFeeRecipient.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessServiceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessServiceResponse.kt
new file mode 100644
index 0000000000..a439b9e05a
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessServiceResponse.kt
@@ -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(
+ @Json(name = "result") val result: T,
+ @Json(name = "success") val isSuccess: Boolean,
+ @Json(name = "timestamp") val timestamp: String,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResultDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResultDTO.kt
new file mode 100644
index 0000000000..8184f49c1c
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSignedTransactionResultDTO.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSupportedTokens.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSupportedTokens.kt
new file mode 100644
index 0000000000..21ef002c64
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessSupportedTokens.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTokenDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTokenDTO.kt
new file mode 100644
index 0000000000..e107fdcc83
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTokenDTO.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt
new file mode 100644
index 0000000000..034bb21097
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/gasless/models/GaslessTransactionRequest.kt
@@ -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,
+)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsArticleDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsArticleDto.kt
index 292b3d4559..ef9e77a8f0 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsArticleDto.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsArticleDto.kt
@@ -27,15 +27,4 @@ data class NewsRelatedTokenDto(
@Json(name = "id") val id: String,
@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,
)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsDetailsResponse.kt
index 230c3f42fb..dd3ccb7001 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsDetailsResponse.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsDetailsResponse.kt
@@ -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,
+ @Json(name = "relatedArticles") val relatedArticles: List,
+)
+
+@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,
)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt
index 773dbb7b72..60484ac83b 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt
@@ -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
diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt
index 9d28f527e7..9651ae0c5c 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt
@@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
data class PromocodeActivationBody(
@Json(name = "promoCode") val promoCode: String,
@Json(name = "address") val address: String,
+ @Json(name = "walletId") val walletId: String,
)
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt
index 7bf7ad5ce5..20d4c632d5 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt
@@ -60,7 +60,15 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
- fun provideNewsConfig(authProvider: AuthProvider): ApiConfig = News(authProvider = authProvider)
+ fun provideNewsConfig(
+ appVersionProvider: AppVersionProvider,
+ authProvider: AuthProvider,
+ appInfoProvider: AppInfoProvider,
+ ): ApiConfig = News(
+ appVersionProvider = appVersionProvider,
+ appInfoProvider = appInfoProvider,
+ authProvider = authProvider,
+ )
@Provides
@IntoSet
@@ -78,13 +86,17 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
- fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider)
+ fun provideTangemPayBffConfig(
+ appVersionProvider: AppVersionProvider,
+ environmentConfigStorage: EnvironmentConfigStorage,
+ ): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage)
@Provides
@IntoSet
- fun provideTangemPayAuthConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPayAuth(
- appVersionProvider,
- )
+ fun provideTangemPayAuthConfig(
+ appVersionProvider: AppVersionProvider,
+ environmentConfigStorage: EnvironmentConfigStorage,
+ ): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage)
@Provides
@IntoSet
@@ -97,4 +109,18 @@ internal object ApiConfigsModule {
fun provideMoonPayConfig(): ApiConfig {
return MoonPay()
}
+
+ @Provides
+ @IntoSet
+ fun provideGaslessServiceConfig(
+ appVersionProvider: AppVersionProvider,
+ authProvider: AuthProvider,
+ appInfoProvider: AppInfoProvider,
+ ): ApiConfig {
+ return GaslessTxService(
+ authProvider = authProvider,
+ appVersionProvider = appVersionProvider,
+ appInfoProvider = appInfoProvider,
+ )
+ }
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt
index 511a2dab3d..25833cc27c 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt
@@ -15,6 +15,7 @@ import com.tangem.datasource.api.moonpay.MoonPayApi
import com.tangem.datasource.api.news.NewsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
+import com.tangem.datasource.api.gasless.GaslessTxServiceApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.TangemPayAuthApi
import com.tangem.datasource.api.stakekit.StakeKitApi
@@ -36,8 +37,11 @@ import javax.inject.Singleton
internal object NetworkModule {
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
+ private const val TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS = 60L
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
+ private const val P2P_ETH_POOL_API_TIMEOUT_SECONDS = 60L
+
@Provides
@Singleton
fun provideApiConfigManager(
@@ -82,6 +86,12 @@ internal object NetworkModule {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.P2PEthPool,
applyTimeoutAnnotations = false,
+ timeouts = Timeouts(
+ callTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
+ connectTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
+ readTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
+ writeTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
+ ),
)
}
@@ -180,4 +190,19 @@ internal object NetworkModule {
applyTimeoutAnnotations = false,
)
}
+
+ @Provides
+ @Singleton
+ fun provideGaslessTxServiceApi(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApi {
+ return retrofitApiBuilder.build(
+ apiConfigId = ApiConfig.ID.GaslessTxService,
+ applyTimeoutAnnotations = false,
+ timeouts = Timeouts(
+ callTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
+ connectTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
+ readTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
+ writeTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
+ ),
+ )
+ }
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt
index fa1dbd3792..22b046ee0c 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt
@@ -3,8 +3,12 @@ package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
import com.tangem.datasource.local.news.details.NewsDetailsStore
+import com.tangem.datasource.local.news.liked.DefaultNewsLikedStore
+import com.tangem.datasource.local.news.liked.NewsLikedStore
import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore
import com.tangem.datasource.local.news.trending.TrendingNewsStore
+import com.tangem.datasource.local.news.viewed.DefaultNewsViewedStore
+import com.tangem.datasource.local.news.viewed.NewsViewedStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -26,4 +30,16 @@ internal object NewsStoreModule {
fun provideTrendingNewsStore(): TrendingNewsStore {
return DefaultTrendingNewsStore(store = RuntimeSharedStore())
}
+
+ @Provides
+ @Singleton
+ fun provideNewsViewedStore(): NewsViewedStore {
+ return DefaultNewsViewedStore(store = RuntimeSharedStore())
+ }
+
+ @Provides
+ @Singleton
+ fun provideNewsLikedStore(): NewsLikedStore {
+ return DefaultNewsLikedStore(store = RuntimeSharedStore())
+ }
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt
index aeb4d1b8d9..d4bd6b3e65 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt
@@ -80,7 +80,7 @@ internal object StakingStoreModule {
@Provides
@Singleton
- fun provideP2PBalancesPersistenceStore(
+ fun provideP2PEthPoolBalancesPersistenceStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
@@ -91,7 +91,7 @@ internal object StakingStoreModule {
types = mapWithStringKeyTypes(valueTypes = setTypes()),
defaultValue = emptyMap(),
),
- produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
+ produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt
index 2c3b8604a0..f1f6854fce 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt
@@ -18,6 +18,7 @@ import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
+import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.datasource.utils.addHeaders
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor
@@ -77,6 +78,7 @@ internal class RetrofitApiBuilder @Inject constructor(
.client(
OkHttpClient.Builder()
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
+ .applyWireMockRedirect()
.let {
if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it
}
@@ -181,6 +183,14 @@ internal class RetrofitApiBuilder @Inject constructor(
)
}
+ private fun OkHttpClient.Builder.applyWireMockRedirect(): OkHttpClient.Builder {
+ return if (BuildConfig.MOCK_DATA_SOURCE) {
+ addInterceptor(interceptor = WireMockRedirectInterceptor())
+ } else {
+ this
+ }
+ }
+
private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder {
if (apiConfigId in excludedApiForLogging) return this
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/accounts/AccountTokenMigrationStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/accounts/AccountTokenMigrationStore.kt
index c527ef5dec..914144ea5a 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/accounts/AccountTokenMigrationStore.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/accounts/AccountTokenMigrationStore.kt
@@ -1,13 +1,14 @@
package com.tangem.datasource.local.accounts
+import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
interface AccountTokenMigrationStore {
- fun get(userWalletId: UserWalletId): Flow?>
+ fun get(userWalletId: UserWalletId): Flow?>
- suspend fun store(userWalletId: UserWalletId, value: Pair)
+ suspend fun store(userWalletId: UserWalletId, value: Pair)
suspend fun remove(userWalletId: UserWalletId)
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/accounts/DefaultAccountTokenMigrationStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/accounts/DefaultAccountTokenMigrationStore.kt
index 920b4ed166..6eba77722f 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/accounts/DefaultAccountTokenMigrationStore.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/accounts/DefaultAccountTokenMigrationStore.kt
@@ -1,19 +1,20 @@
package com.tangem.datasource.local.accounts
import com.tangem.datasource.local.datastore.RuntimeStateStore
+import com.tangem.domain.models.account.AccountName
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
internal class DefaultAccountTokenMigrationStore(
- private val runtimeStateStore: RuntimeStateStore