Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-16 19:58:38 +03:00
commit afff7892bb
721 changed files with 24513 additions and 7275 deletions

View file

@ -18,6 +18,9 @@ android {
jniLibs { jniLibs {
useLegacyPackaging = true useLegacyPackaging = true
} }
resources.excludes.add("META-INF/DEPENDENCIES")
resources.excludes.add("META-INF/LICENSE.md")
resources.excludes.add("META-INF/NOTICE.md")
} }
} }

View file

@ -0,0 +1,8 @@
package com.tangem.common.extensions
import io.github.kakaocup.compose.node.element.KNode
fun KNode.clickWithAssertion() {
assertIsDisplayed()
performClick()
}

View file

@ -0,0 +1,38 @@
package com.tangem.scenarios
import androidx.compose.ui.test.junit4.ComposeTestRule
import com.kaspersky.kaspresso.testcases.api.scenario.Scenario
import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.MainTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.github.kakaocup.compose.node.element.ComposeScreen
class OpenMainScreenScenario(
private val testRule: ComposeTestRule,
private val productType: ProductType? = null,
) : Scenario() {
override val steps: TestContext<Unit>.() -> Unit = {
if (productType != null) {
MockProvider.setMocks(productType)
}
ComposeScreen.onComposeScreen<DisclaimerTestScreen>(testRule) {
step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesTestScreen>(testRule) {
step("Click on \"Scan\" button") {
scanButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<MainTestScreen>(testRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
}
}

View file

@ -0,0 +1,51 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class DetailsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DetailsTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.DETAILS_SCREEN) }
) {
val walletConnectButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.wallet_connect_title))
}
private val walletBlock: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
}
val walletNameButton: KNode = walletBlock.child {
hasClickAction()
hasPosition(0)
}
val scanCardButton: KNode = walletBlock.child {
hasText(getResourceString(R.string.scan_card_settings_button))
}
val buyTangemButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.details_buy_wallet))
}
val appSettingsButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.app_settings_title))
}
val contactSupportButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.details_row_title_contact_to_support))
}
val toSButton: KNode = child {
hasTestTag(TestTags.DETAILS_SCREEN_ITEM)
hasText(getResourceString(R.string.disclaimer_title))
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KButton
object DisclaimerScreen : KScreen<DisclaimerScreen>(){
override val layoutId = R.layout.fragment_disclaimer
override val viewClass = DisclaimerFragment::class.java
val acceptButton: KButton = KButton {
withId(R.id.btn_accept)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
class DisclaimerTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<DisclaimerTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.DISCLAIMER_SCREEN_CONTAINER) }
) {
val acceptButton: KNode = child {
hasTestTag(TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON)
}
}

View file

@ -4,8 +4,8 @@ import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
class WalletScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : class MainTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletScreen>( ComposeScreen<MainTestScreen>(
semanticsProvider = semanticsProvider, semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.WALLET_SCREEN) } viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN) }
) )

View file

@ -8,8 +8,8 @@ import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.views.KView import io.github.kakaocup.kakao.common.views.KView
import io.github.kakaocup.kakao.text.KButton import io.github.kakaocup.kakao.text.KButton
class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : class StoriesTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StoriesScreen>( ComposeScreen<StoriesTestScreen>(
semanticsProvider = semanticsProvider, semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) } viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) }
) { ) {

View file

@ -0,0 +1,16 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
class TestTopBar(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TestTopBar>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.MAIN_SCREEN_TOP_BAR) }
) {
val moreButton: KNode = child {
hasTestTag(TestTags.MAIN_SCREEN_MORE_BUTTON)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class WalletSettingsTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletSettingsTestScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.WALLET_SETTINGS_SCREEN) }
) {
private val walletSettingsItem: KNode = child {
hasTestTag(TestTags.WALLET_SETTINGS_SCREEN_ITEM)
}
val linkMoreCardsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_row_title_create_backup))
}
val cardSettingsButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.card_settings_title))
}
val referralProgramButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.details_referral_title))
}
val forgetWalletButton: KNode = walletSettingsItem.child {
hasText(getResourceString(R.string.settings_forget_wallet))
}
}

View file

@ -0,0 +1,160 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.DetailsTestScreen
import com.tangem.screens.TestTopBar
import com.tangem.screens.WalletSettingsTestScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test
@HiltAndroidTest
class DetailsScreenTest : BaseTestCase() {
@Test
fun walletWithoutBackupDetails() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule))
ComposeScreen.onComposeScreen<TestTopBar>(composeTestRule) {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<DetailsTestScreen>(composeTestRule) {
step("Assert wallet connect button is visible") {
walletConnectButton.assertIsDisplayed()
}
step("Assert scan card button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
walletNameButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
step("Assert Link more cards button is visible") {
linkMoreCardsButton.assertIsDisplayed()
}
step("Assert Card Settings button is visible") {
cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button is visible") {
referralProgramButton.assertIsDisplayed()
}
step("Assert Forget wallet button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
}
@Test
fun wallet2Details() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
ComposeScreen.onComposeScreen<TestTopBar>(composeTestRule) {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<DetailsTestScreen>(composeTestRule) {
step("Assert wallet connect button is visible") {
walletConnectButton.assertIsDisplayed()
}
step("Assert scan card button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
walletNameButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
step("Assert Link more cards button does not exist") {
linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert Card Settings button is visible") {
cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button is visible") {
referralProgramButton.assertIsDisplayed()
}
step("Assert Forget wallet button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
}
@Test
fun noteDetails() =
setupHooks().run {
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Note))
ComposeScreen.onComposeScreen<TestTopBar>(composeTestRule) {
step("Open wallet details") {
moreButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<DetailsTestScreen>(composeTestRule) {
step("Assert wallet connect button does not exist") {
walletConnectButton.assertIsNotDisplayed()
}
step("Assert scan card button is visible") {
scanCardButton.assertIsDisplayed()
}
step("Assert buy Tangem card button is visible") {
buyTangemButton.assertIsDisplayed()
}
step("Assert app settings button is visible") {
appSettingsButton.assertIsDisplayed()
}
step("Assert contact support button is visible") {
contactSupportButton.assertIsDisplayed()
}
step("Assert terms or service button is visible") {
toSButton.assertIsDisplayed()
}
step("Open wallet settings screen") {
walletNameButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<WalletSettingsTestScreen>(composeTestRule) {
step("Assert Link more cards button does not exist") {
linkMoreCardsButton.assertIsNotDisplayed()
}
step("Assert Card Settings button is visible") {
cardSettingsButton.assertIsDisplayed()
}
step("Assert Referral program button does not exist") {
referralProgramButton.assertIsNotDisplayed()
}
step("Assert Forget wallet button is visible") {
forgetWalletButton.assertIsDisplayed()
}
}
}
}

View file

@ -1,11 +1,8 @@
package com.tangem.tests package com.tangem.tests
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerScreen import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.StoriesScreen
import com.tangem.screens.WalletScreen
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test import org.junit.Test
@HiltAndroidTest @HiltAndroidTest
@ -14,26 +11,6 @@ class MainScreenTest : BaseTestCase() {
@Test @Test
fun goToMain() = fun goToMain() =
setupHooks().run { setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) { scenario(OpenMainScreenScenario(composeTestRule))
step("Click on \"Scan\" button") {
scanButton {
assertIsDisplayed()
performClick()
}
}
}
DisclaimerScreen {
step("Click on \"Accept\" button") {
acceptButton {
isVisible()
click()
}
}
}
ComposeScreen.onComposeScreen<WalletScreen>(composeTestRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
} }
} }

View file

@ -1,9 +1,10 @@
package com.tangem.tests package com.tangem.tests
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerScreen import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.StoriesScreen import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.WalletScreen import com.tangem.screens.MainTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.domain.sdk.mocks.MockProvider
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
@ -15,31 +16,22 @@ class ScanErrorTest : BaseTestCase() {
@Test @Test
fun goToMain() = fun goToMain() =
setupHooks().run { setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) { ComposeScreen.onComposeScreen<DisclaimerTestScreen>(composeTestRule) {
step("Click on \"Accept\" button") {
acceptButton.clickWithAssertion()
}
}
ComposeScreen.onComposeScreen<StoriesTestScreen>(composeTestRule) {
step("Click on \"Scan\" button emulating scan error") { step("Click on \"Scan\" button emulating scan error") {
MockProvider.setEmulateError() MockProvider.setEmulateError()
scanButton { scanButton.clickWithAssertion()
assertIsDisplayed()
performClick()
}
} }
step("Click on \"Scan\" button again without emulating error") { step("Click on \"Scan\" button again without emulating error") {
MockProvider.resetEmulateError() MockProvider.resetEmulateError()
scanButton { scanButton.clickWithAssertion()
assertIsDisplayed()
performClick()
}
} }
} }
DisclaimerScreen { ComposeScreen.onComposeScreen<MainTestScreen>(composeTestRule) {
step("Click on \"Accept\" button") {
acceptButton {
isVisible()
click()
}
}
}
ComposeScreen.onComposeScreen<WalletScreen>(composeTestRule) {
step("Make sure wallet screen is visible") { step("Make sure wallet screen is visible") {
assertIsDisplayed() assertIsDisplayed()
} }

View file

@ -2,7 +2,9 @@ package com.tangem.tests
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import com.tangem.common.BaseTestCase import com.tangem.common.BaseTestCase
import com.tangem.screens.StoriesScreen import com.tangem.common.extensions.clickWithAssertion
import com.tangem.screens.DisclaimerTestScreen
import com.tangem.screens.StoriesTestScreen
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen
@ -13,22 +15,16 @@ import org.junit.Test
class StoriesTest : BaseTestCase() { class StoriesTest : BaseTestCase() {
@Test @Test
fun clickOnButtons() = fun clickOnOrderButton() =
setupHooks().run { setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) { ComposeScreen.onComposeScreen<DisclaimerTestScreen>(composeTestRule) {
step("Click on \"Scan\" button") { step("Click on \"Accept\" button") {
scanButton { acceptButton.clickWithAssertion()
assertIsDisplayed()
performClick()
}
}
step("Assert: \"Scan card\" popup opened") {
enableNFCAlert.isDisplayed()
cancelButton.click()
device.uiDevice.pressBack()
} }
}
ComposeScreen.onComposeScreen<StoriesTestScreen>(composeTestRule) {
step("Click on \"Order\" button") { step("Click on \"Order\" button") {
orderButton.performClick() orderButton.clickWithAssertion()
} }
step("Assert: browser opened") { step("Assert: browser opened") {
val expectedIntent = KIntent { val expectedIntent = KIntent {

View file

@ -55,7 +55,7 @@
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/SplashTheme" android:theme="@style/SplashTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustNothing">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

@ -1 +1 @@
Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef Subproject commit 57484a0139299d1229872c626b2c9cb34442038c

View file

@ -708,6 +708,16 @@
"networkId": "cyber/test" "networkId": "cyber/test"
} }
] ]
},
{
"id": "sei-network",
"name": "Sei Network",
"symbol": "SEI",
"networks": [
{
"networkId": "sei-network/test"
}
]
} }
] ]
} }

View file

@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -76,7 +76,7 @@ interface ApplicationEntryPoint {
fun getBalanceHidingRepository(): BalanceHidingRepository fun getBalanceHidingRepository(): BalanceHidingRepository
fun getUserTokensStore(): UserTokensStore fun getAppPreferencesStore(): AppPreferencesStore
fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase

View file

@ -583,8 +583,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun sendStakingUnsubmittedHashes() { private fun sendStakingUnsubmittedHashes() {
lifecycleScope.launch { lifecycleScope.launch {
sendUnsubmittedHashesUseCase.invoke() sendUnsubmittedHashesUseCase.invoke()
.onRight { Timber.d("Submitting hashes succeeded") }
.onLeft { Timber.e(it.toString()) } .onLeft { Timber.e(it.toString()) }
.onRight { Timber.d("Submitting hashes succeeded") }
} }
} }
} }

View file

@ -22,7 +22,7 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val balanceHidingRepository: BalanceHidingRepository private val balanceHidingRepository: BalanceHidingRepository
get() = entryPoint.getBalanceHidingRepository() get() = entryPoint.getBalanceHidingRepository()
private val userTokensStore: UserTokensStore private val appPreferencesStore: AppPreferencesStore
get() = entryPoint.getUserTokensStore() get() = entryPoint.getAppPreferencesStore()
val getAppThemeModeUseCase: GetAppThemeModeUseCase val getAppThemeModeUseCase: GetAppThemeModeUseCase
get() = entryPoint.getGetAppThemeModeUseCase() get() = entryPoint.getGetAppThemeModeUseCase()
@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
} }
derivationsFinder = DerivationsFinder( derivationsFinder = DerivationsFinder(
newTokensStore = userTokensStore, appPreferencesStore = appPreferencesStore,
dispatchers = AppCoroutineDispatcherProvider(), dispatchers = AppCoroutineDispatcherProvider(),
) )
appStateHolder.mainStore = store appStateHolder.mainStore = store

View file

@ -51,6 +51,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
source = state.dialog.source, source = state.dialog.source,
onTryAgain = state.dialog.onTryAgain, onTryAgain = state.dialog.onTryAgain,
) )
is StateDialog.NfcFeatureIsUnavailable -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.nfc_error_unavailable,
context = context,
)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog( is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(

View file

@ -0,0 +1,29 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.core.analytics.AppInstanceIdProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
override suspend fun getAppInstanceId(): String? = suspendCancellableCoroutine { continuation ->
Firebase.analytics.appInstanceId
.addOnSuccessListener { continuation.resume(it) }
.addOnFailureListener {
Timber.w("Fail to get appInstanceId")
continuation.resume(null)
}
}
override fun getAppInstanceIdSync(): String? {
return try {
Firebase.analytics.appInstanceId.result
} catch (e: IllegalStateException) {
Timber.e(e, "getAppInstanceIdSync")
null
}
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.sdk.DefaultSessionViewDelegate import com.tangem.sdk.DefaultSessionViewDelegate
import com.tangem.sdk.extensions.* import com.tangem.sdk.extensions.*
import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider
import com.tangem.sdk.nfc.NfcManager import com.tangem.sdk.nfc.NfcManager
import com.tangem.sdk.storage.create import com.tangem.sdk.storage.create
import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.foregroundActivityObserver
@ -98,9 +99,11 @@ internal class DefaultCardSdkProvider @Inject constructor(
val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity) val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity)
viewDelegate.sdkConfig = config viewDelegate.sdkConfig = config
val androidNfcAvailabilityProvider = AndroidNfcAvailabilityProvider(activity)
val sdk = TangemSdk( val sdk = TangemSdk(
reader = nfcManager.reader, reader = nfcManager.reader,
viewDelegate = viewDelegate, viewDelegate = viewDelegate,
nfcAvailabilityProvider = androidNfcAvailabilityProvider,
secureStorage = secureStorage, secureStorage = secureStorage,
authenticationManager = authenticationManager, authenticationManager = authenticationManager,
keystoreManager = keystoreManager, keystoreManager = keystoreManager,

View file

@ -1,9 +1,11 @@
package com.tangem.tap.di.analytics package com.tangem.tap.di.analytics
import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -23,4 +25,8 @@ internal object AnalyticsModule {
@Provides @Provides
@Singleton @Singleton
fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy() fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
@Provides
@Singleton
fun provideAppInstanceIdProvider(): AppInstanceIdProvider = FirebaseAppInstanceIdProvider()
} }

View file

@ -92,4 +92,12 @@ internal object CardDomainModule {
fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase { fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase {
return NetworkHasDerivationUseCase() return NetworkHasDerivationUseCase()
} }
@Provides
@Singleton
fun provideIsRequiredDerivePublicKeysUseCase(
derivationsRepository: DerivationsRepository,
): HasMissedDerivationsUseCase {
return HasMissedDerivationsUseCase(derivationsRepository)
}
} }

View file

@ -1,7 +1,12 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -17,4 +22,84 @@ internal object ManageTokensDomainModule {
fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase { fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase {
return GetManagedTokensUseCase(manageTokensRepository) return GetManagedTokensUseCase(manageTokensRepository)
} }
@Provides
@Singleton
fun provideValidateTokenFormatUseCase(customTokensRepository: CustomTokensRepository): ValidateTokenFormUseCase {
return ValidateTokenFormUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase {
return CreateCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideFindTokenUseCase(customTokensRepository: CustomTokensRepository): FindTokenUseCase {
return FindTokenUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideCheckIsCurrencyNotAddedUseCase(
customTokensRepository: CustomTokensRepository,
): CheckIsCurrencyNotAddedUseCase {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideRemoveCustomManagedCryptoCurrencyUseCase(
customTokensRepository: CustomTokensRepository,
): RemoveCustomManagedCryptoCurrencyUseCase {
return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideSaveManagedTokensUseCase(
customTokensRepository: CustomTokensRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
derivationsRepository: DerivationsRepository,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
customTokensRepository = customTokensRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
derivationsRepository = derivationsRepository,
)
}
@Provides
@Singleton
fun provideGetSupportedNetworksUseCase(
customTokensRepository: CustomTokensRepository,
): GetSupportedNetworksUseCase {
return GetSupportedNetworksUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideValidateDerivationPathUseCase(
customTokensRepository: CustomTokensRepository,
): ValidateDerivationPathUseCase {
return ValidateDerivationPathUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideCheckHasLinkedTokensUseCase(repository: ManageTokensRepository): CheckHasLinkedTokensUseCase {
return CheckHasLinkedTokensUseCase(repository)
}
@Provides
@Singleton
fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase {
return CheckCurrencyUnsupportedUseCase(repository)
}
} }

View file

@ -1,10 +1,11 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.markets.GetTokenMarketInfoUseCase import com.tangem.domain.markets.*
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.GetTokenQuotesUseCase
import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -37,7 +38,29 @@ object MarketsDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase { fun provideTokenFullQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenFullQuotesUseCase {
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository) return GetTokenFullQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository)
}
@Provides
@Singleton
fun provideSaveMarketTokensUseCase(
derivationsRepository: DerivationsRepository,
marketsTokenRepository: MarketsTokenRepository,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
): SaveMarketTokensUseCase {
return SaveMarketTokensUseCase(
derivationsRepository = derivationsRepository,
marketsTokenRepository = marketsTokenRepository,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
)
} }
} }

View file

@ -57,6 +57,14 @@ internal object SettingsDomainModule {
return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository) return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository)
} }
@Provides
@Singleton
fun provideShouldShowMarketsTooltipUseCase(
settingsRepository: SettingsRepository,
): ShouldShowMarketsTooltipUseCase {
return ShouldShowMarketsTooltipUseCase(settingsRepository = settingsRepository)
}
@Provides @Provides
@Singleton @Singleton
fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase { fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase {

View file

@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.staking.* import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -73,18 +74,6 @@ internal object StakingDomainModule {
) )
} }
@Provides
@Singleton
fun provideGetStakingYieldBalanceUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): GetStakingYieldBalanceUseCase {
return GetStakingYieldBalanceUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideInitializeStakingProcessUseCase( fun provideInitializeStakingProcessUseCase(
@ -112,11 +101,11 @@ internal object StakingDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideSubmitHashUseCase( fun provideSubmitHashUseCase(
stakingRepository: StakingRepository, stakingTransactionHashRepository: StakingTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver, stakingErrorResolver: StakingErrorResolver,
): SubmitHashUseCase { ): SubmitHashUseCase {
return SubmitHashUseCase( return SubmitHashUseCase(
stakingRepository = stakingRepository, stakingTransactionHashRepository = stakingTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver, stakingErrorResolver = stakingErrorResolver,
) )
} }
@ -124,11 +113,11 @@ internal object StakingDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideSaveUnsubmittedHashUseCase( fun provideSaveUnsubmittedHashUseCase(
stakingRepository: StakingRepository, stakingTransactionHashRepository: StakingTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver, stakingErrorResolver: StakingErrorResolver,
): SaveUnsubmittedHashUseCase { ): SaveUnsubmittedHashUseCase {
return SaveUnsubmittedHashUseCase( return SaveUnsubmittedHashUseCase(
stakingRepository = stakingRepository, stakingTransactionHashRepository = stakingTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver, stakingErrorResolver = stakingErrorResolver,
) )
} }
@ -136,23 +125,11 @@ internal object StakingDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideSendUnsubmittedHashesUseCase( fun provideSendUnsubmittedHashesUseCase(
stakingRepository: StakingRepository, stakingTransactionHashRepository: StakingTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver, stakingErrorResolver: StakingErrorResolver,
): SendUnsubmittedHashesUseCase { ): SendUnsubmittedHashesUseCase {
return SendUnsubmittedHashesUseCase( return SendUnsubmittedHashesUseCase(
stakingRepository = stakingRepository, stakingTransactionHashRepository = stakingTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideIsStakeMoreAvailableUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsStakeMoreAvailableUseCase {
return IsStakeMoreAvailableUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver, stakingErrorResolver = stakingErrorResolver,
) )
} }
@ -180,4 +157,16 @@ internal object StakingDomainModule {
stakingErrorResolver = stakingErrorResolver, stakingErrorResolver = stakingErrorResolver,
) )
} }
@Provides
@Singleton
fun provideIsAnyTokenStakedUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsAnyTokenStakedUseCase {
return IsAnyTokenStakedUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
} }

View file

@ -7,6 +7,7 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.* import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.markets.MarketsFeatureToggles
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
@ -38,8 +39,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
networksRepository: NetworksRepository, networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchTokenListUseCase { ): FetchTokenListUseCase {
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
} }
@Provides @Provides
@ -73,8 +75,8 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
networksRepository: NetworksRepository, networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
): GetCardTokensListUseCase { ): GetNodlTokenListUseCase {
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) return GetNodlTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository)
} }
@Provides @Provides
@ -104,6 +106,22 @@ internal object TokensDomainModule {
) )
} }
@Provides
@Singleton
fun provideGetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetAllWalletsCryptoCurrencyStatusesUseCase {
return GetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetCurrencyWarningsUseCase( fun provideGetCurrencyWarningsUseCase(
@ -158,8 +176,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository, currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository, quotesRepository: QuotesRepository,
networksRepository: NetworksRepository, networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCurrencyStatusUseCase { ): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
} }
@Provides @Provides
@ -213,6 +232,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository, networksRepository: NetworksRepository,
stakingRepository: StakingRepository, stakingRepository: StakingRepository,
stakingFeatureToggles: StakingFeatureToggles, stakingFeatureToggles: StakingFeatureToggles,
marketsFeatureToggles: MarketsFeatureToggles,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase { ): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase( return GetCryptoCurrencyActionsUseCase(
@ -224,6 +244,7 @@ internal object TokensDomainModule {
networksRepository = networksRepository, networksRepository = networksRepository,
stakingRepository = stakingRepository, stakingRepository = stakingRepository,
stakingFeatureToggles = stakingFeatureToggles, stakingFeatureToggles = stakingFeatureToggles,
marketsFeatureToggles = marketsFeatureToggles,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
} }
@ -416,7 +437,7 @@ internal object TokensDomainModule {
@Provides @Provides
@Singleton @Singleton
fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase {
return CheckHasLinkedTokensUseCase(currenciesRepository) return GetCurrencyCheckUseCase(currencyChecksRepository)
} }
} }

View file

@ -42,6 +42,21 @@ internal object TransactionDomainModule {
) )
} }
@Provides
@Singleton
fun provideSendMultipleTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
): SendMultipleTransactionUseCase {
return SendMultipleTransactionUseCase(
demoConfig = DemoConfig(),
cardSdkConfigRepository = cardSdkConfigRepository,
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
)
}
@Provides @Provides
@Singleton @Singleton
fun provideAssociateAssetUseCase( fun provideAssociateAssetUseCase(

View file

@ -1,5 +1,7 @@
package com.tangem.tap.domain.card package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
@ -8,10 +10,13 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -30,6 +35,25 @@ internal class DefaultDerivationsRepository(
) : DerivationsRepository { ) : DerivationsRepository {
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) { override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
}
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.ID>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
derivePublicKeysByNetworks(
userWalletId = userWalletId,
networks = networkIds.mapNotNull {
getNetwork(
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
extraDerivationPath = null,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
)
},
)
}
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
@ -38,7 +62,7 @@ internal class DefaultDerivationsRepository(
} }
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.find(currencies) .findByNetworks(networks)
.ifEmpty { .ifEmpty {
Timber.d("Nothing to derive") Timber.d("Nothing to derive")
return return
@ -47,6 +71,26 @@ internal class DefaultDerivationsRepository(
derivePublicKeys(userWalletId = userWalletId, derivations = derivations) derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
} }
override suspend fun hasMissedDerivations(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<Network.ID, String?>,
): Boolean {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (networkId, extraDerivationPath) ->
getNetwork(
blockchain = Blockchain.fromNetworkId(networkId.value) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
)
},
)
return derivations.isNotEmpty()
}
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys { override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response -> .doOnSuccess { response ->

View file

@ -11,6 +11,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>> private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
@ -26,8 +27,12 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
/** Find missed derivations for given currencies [currencies] */ /** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations { fun find(currencies: List<CryptoCurrency>): Derivations {
return currencies.map { it.network }.let(::findByNetworks)
}
fun findByNetworks(networks: List<Network>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> { return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
currencies networks
.mapToNewDerivations() .mapToNewDerivations()
.forEach { data -> .forEach { data ->
val current = this[data.first] val current = this[data.first]
@ -41,25 +46,25 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
} }
} }
private fun List<CryptoCurrency>.mapToNewDerivations(): List<DerivationData> { private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card) val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { currency -> return mapNotNull { network ->
val blockchain = Blockchain.fromId(id = currency.network.id.value) val blockchain = Blockchain.fromId(id = network.id.value)
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, currency = currency) findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
} }
} }
private fun findNewDerivations( private fun findNewDerivations(
curve: EllipticCurve, curve: EllipticCurve,
scanResponse: ScanResponse, scanResponse: ScanResponse,
currency: CryptoCurrency, network: Network,
): DerivationData? { ): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val publicKey = wallet.publicKey.toMapKey() val publicKey = wallet.publicKey.toMapKey()
val derivationCandidates = currency val derivationCandidates = network
.getDerivationCandidates(curve) .getDerivationCandidates(curve)
.ifEmpty { return null } .ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey) .filterAlreadyDerivedKeys(publicKey)
@ -68,13 +73,13 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
return publicKey to derivationCandidates return publicKey to derivationCandidates
} }
private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> { private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = network.id.value) val blockchain = Blockchain.fromId(id = this.id.value)
return buildList { return buildList {
add(blockchain.getDerivationPath(curve = curve)) add(blockchain.getDerivationPath(curve = curve))
add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates)) add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates)) add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
} }
.filterNotNull() .filterNotNull()
.distinct() .distinct()
@ -88,17 +93,17 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
} }
} }
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, currency: CryptoCurrency): DerivationPath? { private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
return if (getSupportedCurves().contains(curve)) { return if (getSupportedCurves().contains(curve)) {
currency.network.derivationPath.value?.let(::DerivationPath) network.derivationPath.value?.let(::DerivationPath)
} else { } else {
null null
} }
} }
private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? { private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) { return if (this == Blockchain.Cardano) {
currency.network.derivationPath.value?.let { network.derivationPath.value?.let {
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
} }
} else { } else {

View file

@ -1,62 +0,0 @@
package com.tangem.tap.domain.configurable.warningMessage
import androidx.annotation.StringRes
import com.squareup.moshi.Json
import com.tangem.blockchain.common.Blockchain
/**
[REDACTED_AUTHOR]
*/
data class WarningMessage(
val title: String,
val message: String,
val type: Type,
val priority: Priority,
val location: List<Location>,
private val blockchains: List<String>?,
@StringRes val titleResId: Int? = null,
@StringRes val messageResId: Int? = null,
val origin: Origin = Origin.Remote,
@StringRes val buttonTextId: Int? = null,
val titleFormatArg: String? = null,
val messageFormatArg: String? = null,
) {
val blockchainList: List<Blockchain>? by lazy {
blockchains?.map { Blockchain.fromId(it.uppercase()) }
}
var isHidden = false
enum class Priority {
@Json(name = "critical")
Critical,
@Json(name = "warning")
Warning,
@Json(name = "info")
Info,
}
enum class Type {
@Json(name = "permanent")
Permanent, // нельзя скрыть
@Json(name = "temporary")
Temporary, // можно скрыть (кнопка ОК)
AppRating,
TestCard,
}
enum class Location {
@Json(name = "send")
SendScreen,
}
enum class Origin {
Remote,
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ProductType
import com.tangem.tap.domain.sdk.mocks.content.NoteMockContent
import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -67,6 +68,7 @@ object MockProvider {
return when (productType) { return when (productType) {
ProductType.Wallet -> WalletMockContent ProductType.Wallet -> WalletMockContent
ProductType.Wallet2 -> Wallet2MockContent ProductType.Wallet2 -> Wallet2MockContent
ProductType.Note -> NoteMockContent
else -> TODO() else -> TODO()
} }
} }

View file

@ -0,0 +1,117 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import java.util.Date
object NoteMockContent : MockContent {
override val cardDto = CardDTO(
cardId = "AB04000000010905",
batchId = "AB04",
cardPublicKey = byteArrayOf(2, 102, 3, -106, -14, -87, -118, 120, 10, 93, 17, 55, 26, -44, 5, 115, 88, 35, 49, -88, -69, 116, 0, -72, -27, 57, 50, -55, 80, -16, 39, -70, 119),
firmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 39,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(-2, -45, 100, -79, 7, -120, 49, 74, 126, -3, -75, 54, -36, -1, 19, -83, 47, -82, 52, -43, -119, 75, 58, 97, 50, 37, 103, -6, -2, 28, 120, 103, -38, -95, 7, -126, -3, -23, -22, -30, -24, -126, 3, 70, 7, -20, -6, -49, 55, -93, 26, 57, -71, 12, -20, 41, -105, -75, 82, 116, 12, -75, -22, 55),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 1,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = false,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = false,
isHDWalletAllowed = false,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = false),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -27, -117, 23, 68, -3, 21, -109, 18, -67, -107, -42, -44, -16, -127, -53, 46, -109, -46, -51, 89, 119, 79, 111, 78, 62, -125, 72, 109, 8, 45, 59, 117),
chainCode = byteArrayOf(),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 3,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Note,
walletData = WalletData(blockchain = "DOGE", token = null),
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(
entries = emptyMap(),
)
override val extendedPublicKey
get() = error("Available only for wallet+?")
override val successResponse = SuccessResponse(cardId = "AB04000000010905")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -1,33 +1,292 @@
package com.tangem.tap.domain.sdk.mocks.content package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.sdk.mocks.MockContent import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import java.util.Date
object Wallet2MockContent : MockContent { object Wallet2MockContent : MockContent {
override val scanResponse: ScanResponse private val primaryCard = PrimaryCard(
get() = TODO("Not yet implemented") cardId = "AF10000000981426",
batchId = "AF10",
cardPublicKey = byteArrayOf(3, 7, 80, -118, 6, 77, -15, -22, 107, 105, -64, 103, 77, -79, -102, 106, 46, 84, 21, -34, 47, -74, -56, 124, 17, -49, -29, 76, 84, 59, 50, -15, -52),
linkingKey = byteArrayOf( //
2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121,
-57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91,
),
existingWalletsCount = 5, isHDWalletAllowed = true,
issuer = Card.Issuer(
name = "TANGEM 2.0",
publicKey = byteArrayOf(2, -120, 89, -52, -60, 36, -73, -17, 103, 107, -110, -36, 3, 110, -122, 72, 43, -38, 8, 30, -50, 25, -23, -17, 38, 94, 5, -112, -20, 9, 54, -24, -32),
),
walletCurves = listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Bls12381G2Aug,
EllipticCurve.Secp256r1,
EllipticCurve.Ed25519Slip0010,
EllipticCurve.Bls12381G2,
EllipticCurve.Bls12381G2Pop,
EllipticCurve.Bip0340,
),
firmwareVersion = FirmwareVersion(
major = 6,
minor = 33,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
isKeysImportAllowed = false,
certificate = null,
)
override val cardDto: CardDTO override val cardDto = CardDTO(
get() = TODO("Not yet implemented") cardId = "AF10000000981426",
batchId = "AF10",
cardPublicKey = byteArrayOf(3, 7, 80, -118, 6, 77, -15, -22, 107, 105, -64, 103, 77, -79, -102, 106, 46, 84, 21, -34, 47, -74, -56, 124, 17, -49, -29, 76, 84, 59, 50, -15, -52),
firmwareVersion = CardDTO.FirmwareVersion(
major = 6,
minor = 33,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1698094800000),
signature = byteArrayOf(71, -20, 9, 31, 25, 111, 61, 119, 109, -123, -63, 51, 58, -71, -44, 53, 57, 20, -16, 97, -87, -82, 1, -35, -48, 63, 77, -78, -89, -112, -27, 25, -10, 90, 7, -53, -84, -125, 112, 68, -14, -85, -100, -64, -115, 31, 42, 119, 87, -79, 127, 42, -87, -102, 13, -9, -10, -51, -29, -63, -1, -52, -117, 57),
),
issuer = CardDTO.Issuer(
name = "TANGEM 2.0",
publicKey = byteArrayOf(2, -120, 89, -52, -60, 36, -73, -17, 103, 107, -110, -36, 3, 110, -122, 72, 43, -38, 8, 30, -50, 25, -23, -17, 38, 94, 5, -112, -20, 9, 54, -24, -32),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 20,
isSettingAccessCodeAllowed = true,
isSettingPasscodeAllowed = true,
isResettingUserCodesAllowed = false,
isLinkedTerminalEnabled = true,
isBackupAllowed = true,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = true,
isHDWalletAllowed = true,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = true,
isPasscodeSet = false,
supportedCurves = listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Bls12381G2Aug,
EllipticCurve.Secp256r1,
EllipticCurve.Ed25519Slip0010,
EllipticCurve.Bls12381G2,
EllipticCurve.Bls12381G2Pop,
EllipticCurve.Bip0340,
),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -114, -64, 120, -121, 11, -28, 89, -91, 114, 10, 84, -87, -36, 36, 19, -69, 95, 66, 14, 32, -35, -99, -67, 118, 51, 26, 71, -78, -36, 59, -126, -58),
chainCode = byteArrayOf(-47, -8, 74, 69, -1, 52, 10, -56, -85, 118, 56, 77, 125, -12, 85, -23, 42, -58, 99, 47, -87, -34, -83, 72, -122, -29, 88, -85, 46, -118, -26, 116),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 73,
remainingSignatures = null,
index = 0,
hasBackup = true,
derivedKeys = mapOf(
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 55, -114, -94, -73, -61, -50, 51, -115, 55, -79, 63, -96, -44, -64, -24, -36, -122, -123, -38, 81, -15, -127, -97, -42, 72, -85, -62, -98, 46, 119, 16, -55),
chainCode = byteArrayOf(31, 17, 71, -28, -29, 17, 72, -29, -98, 112, 31, -8, 72, -75, 4, -11, 60, -100, 9, 35, 58, -42, -38, 96, -71, -68, 24, -119, -43, -18, -122, 72),
),
DerivationPath("m/84'/0'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -26, 103, 96, 112, 127, -125, 0, 7, 53, 30, -12, -82, 45, 14, 107, -9, 126, 75, 104, -67, -49, 35, -12, -82, -90, 101, -101, 125, -76, 88, -54, 99),
chainCode = byteArrayOf(-42, 36, 97, 65, -64, -113, 76, -91, -9, 11, 89, 123, -9, -3, 21, 103, -113, -60, 48, -31, -34, 108, 111, -38, -110, -80, 109, 17, -29, 2, 45, -71),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -96, -3, -83, 101, 63, -25, -125, -4, -65, -42, -56, 24, -52, 118, -11, -104, -105, 40, -59, 20, -109, -97, 29, -95, -6, -80, 2, 67, 103, -80, -22, -94),
chainCode = byteArrayOf(-125, 27, -91, 38, -66, 109, -92, 16, -37, 93, 107, -29, -128, -1, 115, -64, 108, -63, 17, 27, 58, 78, -2, 39, 88, -39, 44, 89, 32, -38, -16, -38),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -114, -64, 120, -121, 11, -28, 89, -91, 114, 10, 84, -87, -36, 36, 19, -69, 95, 66, 14, 32, -35, -99, -67, 118, 51, 26, 71, -78, -36, 59, -126, -58),
chainCode = byteArrayOf(-47, -8, 74, 69, -1, 52, 10, -56, -85, 118, 56, 77, 125, -12, 85, -23, 42, -58, 99, 47, -87, -34, -83, 72, -122, -29, 88, -85, 46, -118, -26, 116),
),
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(-64, 18, -128, 121, -89, -37, -99, 44, -125, -72, -111, -79, 7, 85, 40, 67, -39, 117, 123, 11, 105, -6, -5, -79, 19, -10, -29, 20, -14, -40, 5, 90),
chainCode = byteArrayOf(33, -97, 53, -112, 61, 112, -24, 74, -87, -85, -124, -4, 103, -94, -97, 76, -41, -27, 118, 33, 55, 121, -17, -52, 60, 122, 27, 25, 29, -76, 78, 11),
curve = EllipticCurve.Ed25519,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 1,
hasBackup = true,
derivedKeys = emptyMap(),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(-64, 18, -128, 121, -89, -37, -99, 44, -125, -72, -111, -79, 7, 85, 40, 67, -39, 117, 123, 11, 105, -6, -5, -79, 19, -10, -29, 20, -14, -40, 5, 90),
chainCode = byteArrayOf(33, -97, 53, -112, 61, 112, -24, 74, -87, -85, -124, -4, 103, -94, -97, 76, -41, -27, 118, 33, 55, 121, -17, -52, 60, 122, 27, 25, 29, -76, 78, 11),
),
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(-94, 111, -89, -77, -62, -113, 16, -118, -46, -16, -20, 28, 53, -82, -109, 28, 99, -98, -54, 59, -3, 99, 16, -70, -73, 43, -6, 33, -53, -66, 76, -72, 6, -49, 83, -121, -122, 111, -111, -116, -119, 98, -94, -98, -121, -37, 20, -95),
chainCode = null,
curve = EllipticCurve.Bls12381G2Aug,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 1,
remainingSignatures = null,
index = 2,
hasBackup = true,
derivedKeys = emptyMap(),
extendedPublicKey = null,
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(115, 14, 11, 0, -93, 81, -103, -95, -75, -84, 18, -120, -31, 76, -83, -81, 91, 25, -75, 36, -99, -53, -25, -15, -1, -57, 14, -39, 98, -116, -63, -123),
chainCode = byteArrayOf(23, 5, 38, -48, 67, -42, -31, -21, 89, 11, 22, -28, 44, -19, -115, -78, 123, -27, 57, 57, -24, -86, 55, 15, 104, 114, -36, 80, 81, -108, -41, 112),
curve = EllipticCurve.Bip0340,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 3,
hasBackup = true,
derivedKeys = emptyMap(),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(115, 14, 11, 0, -93, 81, -103, -95, -75, -84, 18, -120, -31, 76, -83, -81, 91, 25, -75, 36, -99, -53, -25, -15, -1, -57, 14, -39, 98, -116, -63, -123),
chainCode = byteArrayOf(23, 5, 38, -48, 67, -42, -31, -21, 89, 11, 22, -28, 44, -19, -115, -78, 123, -27, 57, 57, -24, -86, 55, 15, 104, 114, -36, 80, 81, -108, -41, 112),
),
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(-43, 1, -81, -47, -8, -103, -66, 42, 37, -7, 65, 54, 57, -24, 127, -89, 69, -112, 42, -46, -128, 36, -117, -28, 30, -48, 37, 52, 93, -47, 92, -47),
chainCode = byteArrayOf(4, -97, 81, -37, 76, -67, -87, -4, -82, -36, -45, -28, -117, -59, -62, 93, -73, 50, 65, -91, -83, 25, -95, 89, -64, -40, 113, 28, 59, 113, -99, 89),
curve = EllipticCurve.Ed25519Slip0010,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 15,
remainingSignatures = null,
index = 4,
hasBackup = true,
derivedKeys = emptyMap(),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(-43, 1, -81, -47, -8, -103, -66, 42, 37, -7, 65, 54, 57, -24, 127, -89, 69, -112, 42, -46, -128, 36, -117, -28, 30, -48, 37, 52, 93, -47, 92, -47),
chainCode = byteArrayOf(4, -97, 81, -37, 76, -67, -87, -4, -82, -36, -45, -28, -117, -59, -62, 93, -73, 50, 65, -91, -83, 25, -95, 89, -64, -40, 113, 28, 59, 113, -99, 89),
),
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.Active(1),
)
override val derivationTaskResponse: DerivationTaskResponse override val scanResponse = ScanResponse(
get() = TODO("Not yet implemented") card = cardDto,
productType = ProductType.Wallet2,
walletData = null,
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val extendedPublicKey: ExtendedPublicKey override val derivationTaskResponse = DerivationTaskResponse(
get() = TODO("Not yet implemented") entries = mapOf(
ByteArrayKey(
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),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
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, -67),
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'/145'/0'/0/0") to ExtendedPublicKey( // bch
publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121),
chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge
publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124),
chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
)
override val successResponse: SuccessResponse override val extendedPublicKey = ExtendedPublicKey(
get() = TODO("Not yet implemented") publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110),
chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
)
override val createProductWalletTaskResponse: CreateProductWalletTaskResponse override val successResponse = SuccessResponse(cardId = "AF10000000981426")
get() = TODO("Not yet implemented")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = mapOf(
ByteArrayKey(
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),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
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, -67),
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,
),
),
),
),
primaryCard = primaryCard,
)
override val importWalletResponse: CreateProductWalletTaskResponse override val importWalletResponse: CreateProductWalletTaskResponse
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")

View file

@ -111,6 +111,10 @@ 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), 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), 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'/195'/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( 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), 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),
@ -189,6 +193,13 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0), parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0, childNumber = 0,
), ),
DerivationPath("m/44'/195'/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),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
), ),
), ),
), ),

View file

@ -5,7 +5,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
@ -22,7 +25,7 @@ internal data class BlockchainToDerive(
// FIXME: May be move to DI, currently unnecessary // FIXME: May be move to DI, currently unnecessary
internal class DerivationsFinder( internal class DerivationsFinder(
private val newTokensStore: UserTokensStore, private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) { ) {
@ -64,7 +67,9 @@ internal class DerivationsFinder(
} }
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> { private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
val responseTokens = newTokensStore.getSyncOrNull(userWalletId) val responseTokens = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
?.tokens ?.tokens
?: return hashSetOf() ?: return hashSetOf()

View file

@ -89,7 +89,7 @@ class WalletConnectSdkHelper {
} else { } else {
feeAmount feeAmount
} }
Fee.Ethereum(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) Fee.Ethereum.Legacy(patchedAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger())
} else { } else {
Fee.Common(feeAmount) Fee.Common(feeAmount)
} }

View file

@ -43,7 +43,7 @@ import kotlinx.coroutines.launch
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) { internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed), bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current),
) )
BackHandler( BackHandler(

View file

@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
title = dialog.title.resolveReference(), title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(), message = dialog.description.resolveReference(),
isDismissable = false, isDismissable = false,
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(), title = dialog.confirmText.resolveReference(),
warning = true, warning = true,
onClick = dialog.onConfirm, onClick = dialog.onConfirm,
), ),
dismissButton = DialogButton( dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel), title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss, onClick = dialog.onDismiss,
), ),

View file

@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
title = dialog.title.resolveReference(), title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex, selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(), items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(R.string.common_cancel), title = stringResource(R.string.common_cancel),
onClick = dialog.onDismiss, onClick = dialog.onDismiss,
), ),

View file

@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder
@ -159,6 +160,8 @@ internal class CardSettingsViewModel @Inject constructor(
} }
if (scanResponse.cardTypesResolver.isTangemTwins()) { if (scanResponse.cardTypesResolver.isTangemTwins()) {
// needs to prepare twin state if it's twin cards (depends on onboarding refactoring)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
cardSettingsInteractor.clear() cardSettingsInteractor.clear()
@ -173,13 +176,7 @@ internal class CardSettingsViewModel @Inject constructor(
userWalletId = userWalletId, userWalletId = userWalletId,
cardId = card.cardId, cardId = card.cardId,
isActiveBackupStatus = card.backupStatus?.isActive == true, isActiveBackupStatus = card.backupStatus?.isActive == true,
backupCardsCount = when (val status = card.backupStatus) { backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
CardDTO.BackupStatus.NoBackup,
null,
-> 0
},
), ),
) )
} }

View file

@ -190,11 +190,11 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog( BasicDialog(
title = stringResource(dialog.titleResId), title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId), message = stringResource(dialog.messageResId),
dismissButton = DialogButton( dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel), title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss, onClick = dialog.onDismiss,
), ),
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(id = R.string.card_settings_action_sheet_reset), title = stringResource(id = R.string.card_settings_action_sheet_reset),
warning = true, warning = true,
onClick = dialog.onConfirmClick, onClick = dialog.onConfirmClick,
@ -208,7 +208,7 @@ private fun CompletedResetDialog(dialog: ResetCardDialog) {
BasicDialog( BasicDialog(
title = stringResource(id = dialog.titleResId), title = stringResource(id = dialog.titleResId),
message = stringResource(id = dialog.messageResId), message = stringResource(id = dialog.messageResId),
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok), title = stringResource(id = R.string.common_ok),
onClick = dialog.onConfirmClick, onClick = dialog.onConfirmClick,
), ),

View file

@ -7,18 +7,11 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store import com.tangem.tap.store
@ -32,9 +25,6 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Inject @Inject
override lateinit var uiDependencies: UiDependencies override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var homeFeatureToggles: HomeFeatureToggles
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState) private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private val viewModel by viewModels<HomeViewModel>() private val viewModel by viewModels<HomeViewModel>()
@ -77,31 +67,9 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
private fun ScreenContent() { private fun ScreenContent() {
StoriesScreen( StoriesScreen(
homeState = homeState, homeState = homeState,
onScanButtonClick = { onScanButtonClick = viewModel::onScanClick,
if (homeFeatureToggles.isCallbacksRefactoringEnabled) { onShopButtonClick = viewModel::onShopClick,
viewModel.onScanClick() onSearchTokensClick = viewModel::onSearchClick,
} else {
Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
}
},
onShopButtonClick = {
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
viewModel.onShopClick()
} else {
Analytics.send(IntroductionProcess.ButtonBuyCards())
store.dispatch(HomeAction.GoToShop)
}
},
onSearchTokensClick = {
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
viewModel.onSearchClick()
} else {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatch(TokensAction.SetArgs.ReadAccess)
}
},
) )
} }
} }

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.home
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -45,6 +47,8 @@ internal class HomeViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() { ) : ViewModel() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
fun onScanClick() { fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
scanCard() scanCard()
@ -54,14 +58,16 @@ internal class HomeViewModel @Inject constructor(
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
analyticsEventHandler.send(Shop.ScreenOpened()) analyticsEventHandler.send(Shop.ScreenOpened())
urlOpener.openUrl(NEW_BUY_WALLET_URL) Firebase.analytics.appInstanceId
.addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") }
.addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) }
} }
fun onSearchClick() { fun onSearchClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess) store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) } store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
} }
private fun scanCard() { private fun scanCard() {
@ -79,7 +85,7 @@ internal class HomeViewModel @Inject constructor(
} }
}, },
onFailure = { onFailure = {
Timber.e(it, "Unable to scan card") tangemErrorHandler.onErrorReceived(error = it)
delay(HIDE_PROGRESS_DELAY) delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}, },

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.home
import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.errors.TangemSdkErrorHandler
import org.rekotlin.Store
import timber.log.Timber
class TangemTangemErrorsHandler(val store: Store<AppState>) : TangemSdkErrorHandler {
override fun onErrorReceived(error: TangemError) {
when (error) {
is TangemSdkError -> {
handleCardSdkError(error)
}
is BlockchainError -> {
handleBlockchainSdkError(error)
}
else -> {
Timber.e("Error happened", error)
}
}
}
private fun handleCardSdkError(error: TangemSdkError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> {
store.dispatchOnMain(GlobalAction.ShowDialog(StateDialog.NfcFeatureIsUnavailable))
}
else -> {
Timber.e(error, "Unable to scan card")
}
}
}
private fun handleBlockchainSdkError(error: TangemError) {
Timber.e("Sdk error happened", error)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.features.home.errors
import com.tangem.common.core.TangemError
interface TangemSdkErrorHandler {
fun onErrorReceived(error: TangemError)
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.home.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import javax.inject.Inject
internal class HomeFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isCallbacksRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED")
}

View file

@ -15,7 +15,6 @@ sealed class HomeAction : Action {
data class ReadCard(val scope: CoroutineScope) : HomeAction() data class ReadCard(val scope: CoroutineScope) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data object GoToShop : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction() data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
} }

View file

@ -1,7 +1,5 @@
package com.tangem.tap.features.home.redux package com.tangem.tap.features.home.redux
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.doOnFailure import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess import com.tangem.common.doOnSuccess
@ -15,11 +13,12 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.* import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
@ -61,21 +60,6 @@ private fun handleHomeAction(action: Action) {
readCard() readCard()
} }
} }
is HomeAction.GoToShop -> {
Analytics.send(Shop.ScreenOpened())
Firebase.analytics.appInstanceId
.addOnSuccessListener {
store.dispatchOpenUrl("$NEW_BUY_WALLET_URL&app_instance_id=$it")
}
.addOnFailureListener {
store.dispatchOpenUrl(NEW_BUY_WALLET_URL)
}
// disabled for now in task [REDACTED_JIRA]
// when (action.userCountryCode) {
// RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
// else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
// }
}
} }
} }

View file

@ -123,7 +123,7 @@ internal class MainViewModel @Inject constructor(
private fun fetchStakingTokens() { private fun fetchStakingTokens() {
viewModelScope.launch(dispatchers.main) { viewModelScope.launch(dispatchers.main) {
fetchStakingTokensUseCase() fetchStakingTokensUseCase(true)
.onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
.onRight { Timber.d("Staking token list was fetched successfully") } .onRight { Timber.d("Staking token list was fetched successfully") }
} }

View file

@ -317,16 +317,16 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>(
private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
when (previousStep) { when (previousStep) {
TwinCardsStep.None -> { TwinCardsStep.None, TwinCardsStep.Welcome -> {
when (state.cardNumber) { when (state.cardNumber) {
TwinCardNumber.First -> { TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(false) { twinsWidget.leapfrogWidget.unfold(true) {
twinsWidget.toActivate(false) twinsWidget.toActivate(true)
} }
} }
TwinCardNumber.Second -> { TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) { switchToCard(state.cardNumber, true) {
twinsWidget.toActivate(false) twinsWidget.toActivate(true)
} }
} }
else -> {} else -> {}

View file

@ -23,7 +23,9 @@ sealed class OnboardingWalletAction : Action {
data object ResumeBackup : OnboardingWalletAction() data object ResumeBackup : OnboardingWalletAction()
data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction() data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction()
class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() class SetPrimaryCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
class SetSecondCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
data object OnBackPressed : OnboardingWalletAction() data object OnBackPressed : OnboardingWalletAction()
} }

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri import android.net.Uri
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard import com.tangem.common.extensions.guard
import com.tangem.common.extensions.ifNotNull import com.tangem.common.extensions.ifNotNull
@ -11,6 +12,7 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.common.services.Result import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
@ -106,7 +108,7 @@ private fun handleWalletAction(action: Action) {
val cardPublicKey = backupService.primaryPublicKey val cardPublicKey = backupService.primaryPublicKey
if (primaryCardId != null && cardPublicKey != null) { if (primaryCardId != null && cardPublicKey != null) {
// uses when no scanResponse and backup state restored // uses when no scanResponse and backup state restored
loadArtworkForUnfinishedBackup( loadArtworkForCard(
cardId = primaryCardId, cardId = primaryCardId,
cardPublicKey = cardPublicKey, cardPublicKey = cardPublicKey,
defaultArtwork = action.cardArtworkUriForUnfinishedBackup, defaultArtwork = action.cardArtworkUriForUnfinishedBackup,
@ -119,7 +121,7 @@ private fun handleWalletAction(action: Action) {
.takeIf { it != Artwork.DEFAULT_IMG_URL } .takeIf { it != Artwork.DEFAULT_IMG_URL }
?.let { Uri.parse(it) } ?.let { Uri.parse(it) }
} }
store.dispatchOnMain(OnboardingWalletAction.SetArtworkUrl(cardArtwork)) store.dispatchOnMain(OnboardingWalletAction.SetPrimaryCardArtworkUrl(cardArtwork))
} }
} }
is OnboardingWalletAction.CreateWallet -> { is OnboardingWalletAction.CreateWallet -> {
@ -232,11 +234,7 @@ private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) {
) )
} }
private suspend fun loadArtworkForUnfinishedBackup( private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri {
cardId: String,
cardPublicKey: ByteArray,
defaultArtwork: Uri?,
): Uri {
return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) { return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> { is Result.Success -> {
val artworkId = cardInfo.data.artwork?.id val artworkId = cardInfo.data.artwork?.id
@ -443,6 +441,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false)) store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false))
when (result) { when (result) {
is CompletionResult.Success -> { is CompletionResult.Success -> {
updateArtworks(backupService.addedBackupCardsCount, result.data)
store.dispatchOnMain(BackupAction.AddBackupCard.Success) store.dispatchOnMain(BackupAction.AddBackupCard.Success)
} }
is CompletionResult.Failure -> { is CompletionResult.Failure -> {
@ -630,6 +629,22 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
} }
} }
fun updateArtworks(addedBackupCardsCount: Int, card: Card) {
mainScope.launch {
withIOContext {
val imageUri = loadArtworkForCard(card.cardId, card.cardPublicKey, Uri.EMPTY)
when (addedBackupCardsCount) {
1 -> {
store.dispatchOnMain(OnboardingWalletAction.SetSecondCardArtworkUrl(imageUri))
}
2 -> {
store.dispatchOnMain(OnboardingWalletAction.SetThirdCardArtworkUrl(imageUri))
}
}
}
}
}
internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<String> { internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<String> {
return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.filterNotNull() .filterNotNull()

View file

@ -21,8 +21,20 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
step = OnboardingWalletStep.CreateWallet, step = OnboardingWalletStep.CreateWallet,
) )
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup) is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetArtworkUrl -> { is OnboardingWalletAction.SetPrimaryCardArtworkUrl -> {
state.copy(cardArtworkUri = action.artworkUri) state.copy(
walletImages = state.walletImages.copy(primaryCardImage = action.artworkUri),
)
}
is OnboardingWalletAction.SetSecondCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(secondCardImage = action.artworkUri),
)
}
is OnboardingWalletAction.SetThirdCardArtworkUrl -> {
state.copy(
walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri),
)
} }
is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done) is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done)
else -> state else -> state

View file

@ -12,7 +12,7 @@ data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None, val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null, val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(), val backupState: BackupState = BackupState(),
val cardArtworkUri: Uri? = null, val walletImages: WalletImages = WalletImages(),
val showConfetti: Boolean = false, val showConfetti: Boolean = false,
val isRingOnboarding: Boolean = false, val isRingOnboarding: Boolean = false,
) : StateType { ) : StateType {
@ -49,6 +49,12 @@ data class OnboardingWalletState(
private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0 private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0
} }
data class WalletImages(
val primaryCardImage: Uri? = null,
val secondCardImage: Uri? = null,
val thirdCardImage: Uri? = null,
)
data class OnboardingWallet2State( data class OnboardingWallet2State(
val maxProgress: Int, val maxProgress: Int,
) )

View file

@ -26,6 +26,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic
import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.datasource.utils.isNullOrEmpty
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
@ -186,30 +187,27 @@ class OnboardingWalletFragment :
when { when {
state.wallet2State != null -> { state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel) seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
state.cardArtworkUri?.let { updateWalletImagesState(state.walletImages)
seedPhraseViewModel.setCardArtworkUri(it.toString())
if (state.isRingOnboarding) {
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(it, binding.imvFirstBackupCard)
loadImageIntoImageView(it, binding.imvSecondBackupCard)
}
} }
else -> { else -> {
if (state.isRingOnboarding) { updateWalletImagesState(state.walletImages)
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state) handleOnboardingStep(state)
} }
} }
} }
private fun updateWalletImagesState(walletImages: WalletImages) {
if (!walletImages.primaryCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.primaryCardImage, binding.imvFrontCard)
}
if (!walletImages.secondCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.secondCardImage, binding.imvFirstBackupCard)
}
if (!walletImages.thirdCardImage.isNullOrEmpty()) {
loadImageIntoImageView(walletImages.thirdCardImage, binding.imvSecondBackupCard)
}
}
private fun loadImageIntoImageView(uri: Uri?, view: ImageView) { private fun loadImageIntoImageView(uri: Uri?, view: ImageView) {
view.load(uri) { view.load(uri) {
placeholder(R.drawable.card_placeholder_black) placeholder(R.drawable.card_placeholder_black)

View file

@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Composable @Composable
@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) {
BasicDialog( BasicDialog(
title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title), title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description), message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(R.string.common_enable), title = stringResource(R.string.common_enable),
onClick = dialog.onEnroll, onClick = dialog.onEnroll,
), ),
dismissButton = DialogButton( dismissButton = DialogButtonUM(
onClick = dialog.onCancel, onClick = dialog.onCancel,
), ),
onDismissDialog = dialog.onCancel, onDismissDialog = dialog.onCancel,

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkAddress
@ -48,12 +47,6 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(state, action) is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction(action) is TradeCryptoAction.Sell -> proceedSellAction(action)
is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency)
is TradeCryptoAction.Stake -> openStaking(
userWalletId = action.userWalletId,
cryptoCurrencyId = action.cryptoCurrencyId,
yield = action.yield,
)
is TradeCryptoAction.SendToken -> handleNewSendToken(action = action) is TradeCryptoAction.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action) is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action)
} }
@ -141,22 +134,6 @@ object TradeCryptoMiddleware {
)?.let { store.dispatchOpenUrl(it) } )?.let { store.dispatchOpenUrl(it) }
} }
private fun openSwap(currency: CryptoCurrency) {
store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) }
}
private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) {
store.dispatchNavigationAction {
push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
yield = yield,
),
)
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend( handleNewSend(
userWalletId = action.userWallet.walletId, userWalletId = action.userWallet.walletId,

View file

@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R import com.tangem.wallet.R
@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) {
}, },
), ),
onDismissDialog = warning.onDismiss, onDismissDialog = warning.onDismiss,
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok), title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss, onClick = warning.onDismiss,
), ),
@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) {
title = stringResource(id = R.string.common_attention), title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description), message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss, onDismissDialog = warning.onDismiss,
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok), title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss, onClick = warning.onDismiss,
), ),
@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) {
message = stringResource(id = R.string.biometric_unavailable_warning), message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss, onDismissDialog = warning.onDismiss,
isDismissable = false, isDismissable = false,
confirmButton = DialogButton( confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok), title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss, onClick = warning.onDismiss,
), ),

View file

@ -11,6 +11,7 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.ManageTokensToggles import com.tangem.features.managetokens.ManageTokensToggles
import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.api.navigation.SendRouter
@ -50,6 +51,7 @@ internal class ChildFactory @Inject constructor(
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val sendRouter: SendRouter, private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter, private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter, private val walletRouter: WalletRouter,
@ -126,13 +128,7 @@ internal class ChildFactory @Inject constructor(
if (manageTokensToggles.isFeatureEnabled) { if (manageTokensToggles.isFeatureEnabled) {
route.asComponentChild( route.asComponentChild(
contextProvider = contextProvider(route, contextFactory), contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params( params = ManageTokensComponent.Params(route.userWalletId),
mode = if (route.readOnlyContent) {
ManageTokensComponent.Mode.READ_ONLY
} else {
ManageTokensComponent.Mode.MANAGE
},
),
componentFactory = manageTokensComponentFactory, componentFactory = manageTokensComponentFactory,
) )
} else { } else {
@ -191,6 +187,23 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory, componentFactory = walletSettingsComponentFactory,
) )
} }
is AppRoute.MarketsTokenDetails -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = MarketsTokenDetailsComponent.Params(
token = route.token,
appCurrency = route.appCurrency,
showPortfolio = route.showPortfolio,
analyticsParams = route.analyticsParams?.let {
MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = it.blockchain,
source = it.source,
)
},
),
componentFactory = marketsTokenDetailsComponentFactory,
)
}
} }
} }

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/llTotalContainer"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/llTotal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/send_total_label"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotalValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textSize="20sp"
android:textStyle="bold"
tools:text="usd" />
</FrameLayout>
<TextView
android:id="@+id/tvWillBeSentValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:gravity="end"
android:textColor="@color/text_tertiary"
tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" />
</LinearLayout>
<FrameLayout
android:id="@+id/flTotalTokenCrypto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:id="@+id/tvTotalTokenCrypto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/send_total_label"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotalTokenCryptoValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="usd" />
</FrameLayout>
</LinearLayout>

View file

@ -1,8 +1,12 @@
package com.tangem.tap.domain.card package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetworkDerivationPath
import com.tangem.data.common.currency.getNetworkStandardType
import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
@ -40,11 +44,23 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
} }
private fun createCoin(blockchain: Blockchain): CryptoCurrency { private fun createCoin(blockchain: Blockchain): CryptoCurrency {
return factory.createCoin( val network = Network(
blockchain = blockchain, id = Network.ID(blockchain.id),
extraDerivationPath = null, backendId = blockchain.toNetworkId(),
derivationStyleProvider = scanResponse.derivationStyleProvider, name = blockchain.getNetworkName(),
)!! isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
blockchain,
extraDerivationPath = null,
scanResponse.derivationStyleProvider,
),
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = false,
)
return factory.createCoin(network = network)
} }
// Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood // Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood
@ -68,6 +84,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
isTestnet = false, isTestnet = false,
standardType = Network.StandardType.ERC20, standardType = Network.StandardType.ERC20,
hasFiatFeeRate = true, hasFiatFeeRate = true,
canHandleTokens = true,
), ),
name = "NEVER-MIND", name = "NEVER-MIND",
symbol = "NEVER-MIND", symbol = "NEVER-MIND",

View file

@ -19,6 +19,8 @@ dependencies {
implementation(projects.domain.tokens.models) implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models) implementation(projects.domain.staking.models)
implementation(projects.domain.markets.models)
implementation(projects.domain.appCurrency.models)
/* Libs - Other */ /* Libs - Other */
api(deps.kotlin.serialization) api(deps.kotlin.serialization)

View file

@ -5,6 +5,8 @@ import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.SerializableIntent import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
@ -177,8 +179,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class ManageTokens( data class ManageTokens(
val readOnlyContent: Boolean, val userWalletId: UserWalletId? = null,
) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams { ) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer()) override fun getBundle(): Bundle = bundle(serializer())
} }
@ -217,12 +219,14 @@ sealed class AppRoute(val path: String) : Route {
@Serializable @Serializable
data class Swap( data class Swap(
val currency: CryptoCurrency, val currency: CryptoCurrency,
) : AppRoute(path = "/swap/${currency.id.value}"), RouteBundleParams { val userWalletId: UserWalletId,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer()) override fun getBundle(): Bundle = bundle(serializer())
companion object { companion object {
const val CURRENCY_BUNDLE_KEY = "currency" const val CURRENCY_BUNDLE_KEY = "currency"
const val USER_WALLET_ID_KEY = "userWalletId"
} }
} }
@ -262,4 +266,19 @@ sealed class AppRoute(val path: String) : Route {
data class WalletSettings( data class WalletSettings(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}") ) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}")
@Serializable
data class MarketsTokenDetails(
val token: TokenMarketParams,
val appCurrency: AppCurrency,
val showPortfolio: Boolean,
val analyticsParams: AnalyticsParams? = null,
) : AppRoute(path = "/markets_token_details/${token.id}/$showPortfolio") {
@Serializable
data class AnalyticsParams(
val blockchain: String?,
val source: String,
)
}
} }

View file

@ -0,0 +1,51 @@
package com.tangem.common.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
/**
* Temporary solution to convert [AppRouter] to [Router].
* (through manual ComponentContext creation).
*
* **Will be removed when all screens will be migrated to Decompose.**
*
* @return [Router] that wraps [AppRouter].
*/
fun AppRouter.asRouter(): Router {
return RouterProxy(appRouter = this)
}
private class RouterProxy(
private val appRouter: AppRouter,
) : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is AppRoute) {
appRouter.push(route, onComplete)
}
}
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
routes.filterIsInstance<AppRoute>().let {
appRouter.replaceAll(*it.toTypedArray(), onComplete = onComplete)
}
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.pop(onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is AppRoute) {
appRouter.popTo(route, onComplete)
}
}
@Suppress("UNCHECKED_CAST")
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.popTo(routeClass as KClass<out AppRoute>, onComplete)
}
}

View file

@ -125,6 +125,8 @@ fun MarketChart(
// Sometimes the chart is not drawn correctly (ex. in LazyLayout), so we need to force the redraw // Sometimes the chart is not drawn correctly (ex. in LazyLayout), so we need to force the redraw
.drawBehind { .drawBehind {
state.markerFraction state.markerFraction
state.chartColor
state.markerHighlightRightSide
}, },
chart = chart, chart = chart,
modelProducer = state.modelProducer, modelProducer = state.modelProducer,

View file

@ -0,0 +1,13 @@
package com.tangem.common.ui.charts.state
import kotlinx.collections.immutable.toImmutableList
fun MarketChartData.Data.sorted(): MarketChartData.Data {
val points = this.x.zip(this.y).sortedBy { it.first }
val (x, y) = points.unzip()
return MarketChartData.Data(
x = x.toImmutableList(),
y = y.toImmutableList(),
)
}

View file

@ -18,6 +18,7 @@ dependencies {
implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.tooling)
implementation(deps.compose.navigation) implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt) implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
/** Deps */ /** Deps */
implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.immutable.collections)
@ -27,9 +28,14 @@ dependencies {
implementation(projects.core.utils) implementation(projects.core.utils)
/** Project - Domain */ /** Project - Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models) implementation(projects.domain.appCurrency.models)
implementation(projects.domain.legacy)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) { implementation(deps.tangem.blockchain) {
exclude(module = "joda-time") exclude(module = "joda-time")
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
package com.tangem.common.ui.amountScreen package com.tangem.common.ui.amountScreen
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,24 +19,28 @@ import com.tangem.core.ui.res.TangemThemePreview
/** /**
* Amount screen with field * Amount screen with field
* @param amountState amount state * @param amountState amount state
* @param isBalanceHiding flag hidden balances * @param isBalanceHidden flag hidden balances
* @param clickIntents amount screen clicks * @param clickIntents amount screen clicks
*/ */
@Composable @Composable
fun AmountScreenContent(amountState: AmountState, isBalanceHiding: Boolean, clickIntents: AmountScreenClickIntents) { fun AmountScreenContent(
amountState: AmountState,
isBalanceHidden: Boolean,
clickIntents: AmountScreenClickIntents,
modifier: Modifier = Modifier,
) {
if (amountState !is AmountState.Data) return if (amountState !is AmountState.Data) return
// Do not put fillMaxSize() in here // Do not put fillMaxSize() in here
LazyColumn( LazyColumn(
modifier = Modifier modifier = modifier
.background(TangemTheme.colors.background.tertiary)
.padding( .padding(
start = TangemTheme.dimens.spacing16, start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16,
), ),
) { ) {
amountField(amountState = amountState, isBalanceHiding = isBalanceHiding) amountField(amountState = amountState, isBalanceHidden = isBalanceHidden)
buttons( buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig, segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents, clickIntents = clickIntents,
@ -57,7 +60,7 @@ private fun SendAmountContentPreview(
TangemThemePreview { TangemThemePreview {
AmountScreenContent( AmountScreenContent(
amountState = amountState, amountState = amountState,
isBalanceHiding = false, isBalanceHidden = false,
clickIntents = AmountScreenClickIntentsStub, clickIntents = AmountScreenClickIntentsStub,
) )
} }

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters.field package com.tangem.common.ui.amountScreen.converters.field
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
@ -8,6 +9,7 @@ import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
import com.tangem.common.ui.amountScreen.utils.getCryptoValue import com.tangem.common.ui.amountScreen.utils.getCryptoValue
import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getFiatValue
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -61,7 +63,8 @@ class AmountFieldChangeTransformer(
value = cryptoValue, value = cryptoValue,
fiatValue = fiatValue, fiatValue = fiatValue,
isError = isExceedBalance, isError = isExceedBalance,
error = resourceReference(R.string.send_validation_amount_exceeds_balance), error = resourceReference(R.string.send_validation_amount_exceeds_balance).takeIf { isExceedBalance }
?: TextReference.EMPTY,
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
@ -81,6 +84,10 @@ class AmountFieldChangeTransformer(
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
isError = false, isError = false,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.None,
keyboardType = KeyboardType.Number,
),
), ),
) )
} }

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
@ -62,7 +61,8 @@ class AmountFieldConverter(
cryptoAmount = cryptoAmount, cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()),
isError = false, isError = false,
error = TextReference.Res(R.string.send_validation_amount_exceeds_balance), isWarning = false,
error = TextReference.EMPTY,
isFiatUnavailable = fiatRate == null, isFiatUnavailable = fiatRate == null,
isValuePasted = false, isValuePasted = false,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,

View file

@ -35,5 +35,6 @@ data class AmountFieldModel(
val isValuePasted: Boolean, val isValuePasted: Boolean,
val onValuePastedTriggerDismiss: () -> Unit, val onValuePastedTriggerDismiss: () -> Unit,
val isError: Boolean, val isError: Boolean,
val isWarning: Boolean,
val error: TextReference, val error: TextReference,
) )

View file

@ -53,6 +53,7 @@ object AmountStatePreviewData {
fiatValue = "123.123", fiatValue = "123.123",
isFiatUnavailable = false, isFiatUnavailable = false,
isError = false, isError = false,
isWarning = false,
error = TextReference.EMPTY, error = TextReference.EMPTY,
isValuePasted = false, isValuePasted = false,
onValuePastedTriggerDismiss = {}, onValuePastedTriggerDismiss = {},

View file

@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -112,6 +115,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
) )
AmountFieldError( AmountFieldError(
isError = amountField.isError, isError = amountField.isError,
isWarning = amountField.isWarning,
error = amountField.error, error = amountField.error,
modifier = Modifier modifier = Modifier
.align(BottomCenter) .align(BottomCenter)
@ -124,17 +128,24 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
} }
@Composable @Composable
private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { private fun AmountFieldError(
isError: Boolean,
isWarning: Boolean,
error: TextReference,
modifier: Modifier = Modifier,
) {
AnimatedVisibility( AnimatedVisibility(
visible = isError, visible = isError || isWarning,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), exit = fadeOut(),
modifier = modifier, modifier = modifier,
) { ) {
val errorText = remember(this) { error }
val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention
Text( Text(
text = error.resolveReference(), text = errorText.resolveReference(),
style = TangemTheme.typography.caption2, style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.warning, color = color,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} }

View file

@ -14,15 +14,15 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.StringsSigns.STARS
private const val AMOUNT_FIELD_KEY = "amountFieldKey" private const val AMOUNT_FIELD_KEY = "amountFieldKey"
internal fun LazyListScope.amountField( internal fun LazyListScope.amountField(
amountState: AmountState.Data, amountState: AmountState.Data,
isBalanceHiding: Boolean, isBalanceHidden: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
item(key = AMOUNT_FIELD_KEY) { item(key = AMOUNT_FIELD_KEY) {
@ -41,7 +41,7 @@ internal fun LazyListScope.amountField(
.padding(top = TangemTheme.dimens.spacing14), .padding(top = TangemTheme.dimens.spacing14),
) )
val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference() val balance = amountState.walletBalance.orMaskWithStars(isBalanceHidden).resolveReference()
AnimatedContent( AnimatedContent(
targetState = balance, targetState = balance,
label = "Hide Balance Animation", label = "Hide Balance Animation",

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -59,7 +58,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
BasicDialog( BasicDialog(
message = content.data.dialogText.resolveReference(), message = content.data.dialogText.resolveReference(),
title = stringResource(id = R.string.common_approve), title = stringResource(id = R.string.common_approve),
confirmButton = DialogButton { isPermissionAlertShow = false }, confirmButton = DialogButtonUM { isPermissionAlertShow = false },
onDismissDialog = {}, onDismissDialog = {},
) )
} }
@ -166,7 +165,7 @@ private fun AmountItem(
.clickable( .clickable(
enabled = onChangeApproveType != null, enabled = onChangeApproveType != null,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(), indication = ripple(),
onClick = { isExpandSelector = true }, onClick = { isExpandSelector = true },
), ),
) { ) {

View file

@ -2,27 +2,32 @@ package com.tangem.common.ui.navigationButtons
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.material3.Text
import androidx.compose.runtime.remember import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -30,39 +35,34 @@ import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifier = Modifier) { fun NavigationButtonsBlock(
buttonState: NavigationButtonsState,
modifier: Modifier = Modifier,
footerText: TextReference? = null,
) {
val state = buttonState as? NavigationButtonsState.Data val state = buttonState as? NavigationButtonsState.Data
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
) { ) {
InfoText(footerText)
ExtraButtons(state?.extraButtons, state?.txUrl) ExtraButtons(state?.extraButtons, state?.txUrl)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) { ) {
PreviousButton(state?.prevButton) PreviousButton(state?.prevButton)
PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
} }
SecondaryButton(state?.secondaryButton)
} }
} }
@Composable @Composable
private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
val wrappedButton by rememberNavigationButton(primaryButton)
AnimatedContent( AnimatedContent(
targetState = primaryButton, targetState = wrappedButton,
transitionSpec = { transitionSpec = { navigationButtonsTransition() },
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
label = "Animate show primary button", label = "Animate show primary button",
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
@ -88,43 +88,6 @@ private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier =
} }
} }
@Composable
private fun SecondaryButton(secondaryButton: NavigationButton?) {
AnimatedContent(
targetState = secondaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
contentAlignment = Alignment.Center,
label = "Animate show secondary button",
modifier = Modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) }
?: TangemButtonIconPosition.None
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
icon = icon,
showProgress = button.showProgress,
colors = TangemButtonsDefaults.secondaryButtonColors,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}
}
}
@Composable @Composable
private fun PreviousButton(prevButton: NavigationButton?) { private fun PreviousButton(prevButton: NavigationButton?) {
AnimatedVisibility( AnimatedVisibility(
@ -182,6 +145,61 @@ private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl:
} }
} }
@Composable
private fun InfoText(footerText: TextReference?, modifier: Modifier = Modifier) {
var isVisibleProxy by remember { mutableStateOf(!footerText.isNullOrEmpty()) }
val keyboard by keyboardAsState()
// the text should appear when the keyboard is closed
LaunchedEffect(footerText, keyboard) {
if (footerText.isNullOrEmpty() && keyboard is Keyboard.Opened) {
return@LaunchedEffect
}
isVisibleProxy = !footerText.isNullOrEmpty()
}
AnimatedVisibility(
visible = isVisibleProxy,
modifier = modifier,
enter = slideInVertically() + fadeIn(),
exit = fadeOut(tween(durationMillis = 300)),
label = "Animate footer text appearance",
) {
val text = remember(this) { requireNotNull(footerText) }
Text(
text = text.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing12),
)
}
}
@Composable
private fun rememberNavigationButton(button: NavigationButton?): MutableState<NavigationButton?> {
return remember(
button?.iconRes,
button?.isIconVisible,
button?.isEnabled,
button?.showProgress,
button?.textReference,
) { mutableStateOf(button) }
}
private fun <T> AnimatedContentTransitionScope<T>.navigationButtonsTransition(): ContentTransform {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
return if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
}
// region Preview // region Preview
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -10,9 +10,9 @@ sealed class NavigationButtonsState {
data class Data( data class Data(
val primaryButton: NavigationButton, val primaryButton: NavigationButton,
val prevButton: NavigationButton?, val prevButton: NavigationButton?,
val secondaryButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>, val extraButtons: ImmutableList<NavigationButton>,
val txUrl: String? = null, val txUrl: String? = null,
val onTextClick: (String) -> Unit,
) : NavigationButtonsState() ) : NavigationButtonsState()
} }

View file

@ -30,14 +30,6 @@ internal object NavigationButtonsPreview {
), ),
) )
private val next = NavigationButton(
textReference = resourceReference(R.string.common_next),
isSecondary = false,
isIconVisible = false,
showProgress = false,
isEnabled = true,
onClick = {},
)
private val prev = NavigationButton( private val prev = NavigationButton(
textReference = TextReference.EMPTY, textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24, iconRes = R.drawable.ic_back_24,
@ -60,8 +52,8 @@ internal object NavigationButtonsPreview {
val allButtons = NavigationButtonsState.Data( val allButtons = NavigationButtonsState.Data(
primaryButton = finished, primaryButton = finished,
prevButton = prev, prevButton = prev,
secondaryButton = next,
extraButtons = extraButtons, extraButtons = extraButtons,
txUrl = "https://tangem.com", txUrl = "https://tangem.com",
onTextClick = {},
) )
} }

View file

@ -0,0 +1,302 @@
package com.tangem.common.ui.notifications
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.ui.R
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.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import java.math.BigDecimal
sealed class NotificationUM(val config: NotificationConfig) {
open class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonState,
onCloseClick = onCloseClick,
),
) {
data object TotalExceedsBalance : Error(
title = resourceReference(R.string.send_notification_exceed_balance_title),
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
)
data object InvalidAmount : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
)
data class MinimumAmountError(val amount: String) : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(
R.string.send_notification_invalid_minimum_amount_text,
wrappedList(amount, amount),
),
)
data class TransactionLimitError(
val cryptoCurrency: String,
val utxoLimit: String,
val amountLimit: String,
val onConfirmClick: () -> Unit,
) : Error(
title = resourceReference(R.string.send_notification_transaction_limit_title),
subtitle = resourceReference(
R.string.send_notification_transaction_limit_text,
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)),
onClick = onConfirmClick,
),
)
data class TokenExceedsBalance(
val networkIconId: Int,
val currencyName: String,
val feeName: String,
val feeSymbol: String,
val networkName: String,
val mergeFeeNetworkName: Boolean = false,
val onClick: (() -> Unit)? = null,
) : Error(
title = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_title,
wrappedList(feeName),
),
subtitle = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
),
iconResId = networkIconId,
buttonState = onClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
R.string.common_buy_currency,
wrappedList(
if (mergeFeeNetworkName) {
"$currencyName ($feeSymbol)"
} else {
feeName
},
),
),
onClick = onClick,
)
},
)
data class ExceedsBalance(
val networkIconId: Int,
val currencyName: String,
val feeName: String,
val feeSymbol: String,
val networkName: String,
val mergeFeeNetworkName: Boolean = false,
val onClick: (() -> Unit)? = null,
) : Error(
title = resourceReference(
id = R.string.warning_blocked_funds_for_fee_title,
wrappedList(feeName),
),
subtitle = resourceReference(
id = R.string.warning_blocked_funds_for_fee_message,
formatArgs = wrappedList(currencyName),
),
iconResId = networkIconId,
buttonState = onClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
R.string.common_buy_currency,
wrappedList(
if (mergeFeeNetworkName) {
"$currencyName ($feeSymbol)"
} else {
feeName
},
),
),
onClick = onClick,
)
},
)
data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error(
title = resourceReference(R.string.send_notification_existential_deposit_title),
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)),
onClick = onConfirmClick,
),
)
data class ReserveAmount(val amount: String) : Error(
title = resourceReference(
id = R.string.send_notification_invalid_reserve_amount_title,
wrappedList(amount),
),
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
)
}
open class Warning(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.img_attention_20,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
),
) {
data class HighFeeError(
val currencyName: String,
val amount: String,
val onConfirmClick: () -> Unit,
val onCloseClick: () -> Unit,
) : Warning(
title = resourceReference(R.string.send_notification_high_fee_title),
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
onClick = onConfirmClick,
),
onCloseClick = onCloseClick,
)
data object FeeTooLow : Warning(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
)
data class TooHigh(
val value: String,
) : Warning(
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
)
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
data class TronAccountNotActivated(val tokenName: String) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(
R.string.send_tron_account_activation_error,
wrappedList(tokenName),
),
)
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(
R.string.common_network_fee_warning_content,
wrappedList(cryptoAmount, fiatAmount),
),
)
}
open class Info(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_circle_24,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
),
)
sealed interface Cardano {
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning(
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
subtitle = resourceReference(
id = R.string.cardano_coin_will_be_send_with_token_description,
formatArgs = wrappedList(minAdaValue, tokenName),
),
)
data object InsufficientBalanceToTransferCoin : Error(
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
)
data class InsufficientBalanceToTransferToken(val tokenName: String) : Error(
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
subtitle = resourceReference(
id = R.string.cardano_insufficient_balance_to_send_token_description,
formatArgs = wrappedList(tokenName),
),
)
}
sealed interface Koinos {
data class InsufficientRecoverableMana(
val mana: BigDecimal,
val maxMana: BigDecimal,
) : Error(
title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title),
subtitle = resourceReference(
R.string.koinos_insufficient_mana_to_send_koin_description,
formatArgs = wrappedList(
BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()),
BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()),
),
),
)
data object InsufficientBalance : Error(
title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title),
subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description),
)
data class ManaExceedsBalance(
val availableKoinForTransfer: BigDecimal,
val onReduceClick: () -> Unit,
) : Error(
title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title),
subtitle = resourceReference(
R.string.koinos_mana_exceeds_koin_balance_description,
formatArgs = wrappedList(
BigDecimalFormatter.formatCryptoAmount(
availableKoinForTransfer,
Blockchain.Koinos.currency,
Blockchain.Koinos.decimals(),
),
),
),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)),
onClick = onReduceClick,
),
)
}
}

View file

@ -0,0 +1,363 @@
package com.tangem.common.ui.notifications
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
@Suppress("LargeClass")
object NotificationsFactory {
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
feeError: GetFeeError,
tokenName: String,
onReload: () -> Unit,
) {
when (feeError) {
is GetFeeError.BlockchainErrors.TronActivationError -> add(
NotificationUM.Warning.TronAccountNotActivated(tokenName),
)
is GetFeeError.DataError,
is GetFeeError.UnknownError,
-> add(
NotificationUM.Warning.NetworkFeeUnreachable(onReload),
)
else -> {
/* do nothing */
}
}
}
fun MutableList<NotificationUM>.addExceedBalanceNotification(
feeAmount: BigDecimal,
sendingAmount: BigDecimal,
isSubtractionAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
minimumRequirement: BigDecimal? = null,
) {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
if (!isSubtractionAvailable) return
val showNotification = sendingAmount + feeAmount > balance - minimumRequirement.orZero()
if (showNotification) {
add(NotificationUM.Error.TotalExceedsBalance)
}
}
fun MutableList<NotificationUM>.addReserveAmountErrorNotification(
reserveAmount: BigDecimal?,
sendingAmount: BigDecimal,
cryptoCurrency: CryptoCurrency,
isAccountFunded: Boolean,
) {
if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) {
add(
NotificationUM.Error.ReserveAmount(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = sendingAmount,
cryptoCurrency = cryptoCurrency,
),
),
)
}
}
fun MutableList<NotificationUM>.addTransactionLimitErrorNotification(
utxoLimit: UtxoAmountLimit?,
cryptoCurrency: CryptoCurrency,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
if (utxoLimit != null) {
add(
NotificationUM.Error.TransactionLimitError(
cryptoCurrency = cryptoCurrency.name,
utxoLimit = utxoLimit.maxLimit.toPlainString(),
amountLimit = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = utxoLimit.maxAmount,
cryptoCurrency = cryptoCurrency,
),
onConfirmClick = {
onReduceClick(
utxoLimit.maxAmount,
NotificationUM.Error.TransactionLimitError::class.java,
)
},
),
)
}
}
fun MutableList<NotificationUM>.addExistentialWarningNotification(
existentialDeposit: BigDecimal?,
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
onReduceClick: (
reduceAmountBy: BigDecimal,
reduceAmountByDiff: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val balance = cryptoCurrencyStatus.value.amount ?: return
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
feeAmount
} else {
receivedAmount
}
val diff = balance.minus(spendingAmount)
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {
add(
NotificationUM.Error.ExistentialDeposit(
deposit = BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = existentialDeposit,
cryptoCurrency = cryptoCurrency,
),
onConfirmClick = {
onReduceClick(
existentialDeposit,
existentialDeposit.minus(diff),
NotificationUM.Error.ExistentialDeposit::class.java,
)
},
),
)
}
}
fun MutableList<NotificationUM>.addFeeCoverageNotification(
isFeeCoverage: Boolean,
amountField: AmountFieldModel,
sendingValue: BigDecimal,
appCurrency: AppCurrency,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val amountValue = amountField.cryptoAmount.value ?: return
val cryptoDiff = amountValue.minus(sendingValue)
if (isFeeCoverage) {
add(
NotificationUM.Warning.FeeCoverageNotification(
cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = cryptoDiff,
cryptoCurrency = cryptoCurrency,
),
fiatAmount = getFiatString(
value = cryptoDiff,
rate = fiatRate,
appCurrency = appCurrency,
),
),
)
}
}
fun MutableList<NotificationUM>.addDustWarningNotification(
dustValue: BigDecimal?,
feeValue: BigDecimal,
sendingAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
if (dustValue == null) return
val isExceedsLimit = checkDustLimits(
feeAmount = feeValue,
receivedAmount = sendingAmount,
dustValue = dustValue,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
)
if (isExceedsLimit) {
add(
NotificationUM.Error.MinimumAmountError(
amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals),
),
)
}
}
fun MutableList<NotificationUM>.addExceedsBalanceNotification(
cryptoCurrencyWarning: CryptoCurrencyWarning?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
shouldMergeFeeNetworkName: Boolean,
onClick: (CryptoCurrency) -> Unit,
onAnalyticsEvent: (CryptoCurrency) -> Unit,
) {
when (cryptoCurrencyWarning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
NotificationUM.Error.TokenExceedsBalance(
networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId,
networkName = cryptoCurrencyWarning.coinCurrency.name,
currencyName = cryptoCurrencyStatus.currency.name,
feeName = cryptoCurrencyWarning.coinCurrency.name,
feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol,
mergeFeeNetworkName = shouldMergeFeeNetworkName,
onClick = {
onClick(cryptoCurrencyWarning.coinCurrency)
},
),
)
onAnalyticsEvent(cryptoCurrencyStatus.currency)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = cryptoCurrencyWarning.feeCurrency
add(
NotificationUM.Error.TokenExceedsBalance(
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
currencyName = cryptoCurrencyWarning.currency.name,
feeName = cryptoCurrencyWarning.feeCurrencyName,
feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol,
networkName = cryptoCurrencyWarning.networkName,
mergeFeeNetworkName = shouldMergeFeeNetworkName,
onClick = {
currency?.let {
onClick(currency)
}
},
),
)
onAnalyticsEvent(cryptoCurrencyWarning.currency)
}
else -> Unit
}
}
fun MutableList<NotificationUM>.addValidateTransactionNotifications(
dustValue: BigDecimal,
fee: Fee?,
validationError: Throwable?,
cryptoCurrency: CryptoCurrency,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
when (validationError) {
is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError(
error = validationError,
sendingCurrency = cryptoCurrency,
dustValue = dustValue,
)
is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError(
error = validationError,
onReduceClick = onReduceClick,
)
null -> (fee as? Fee.CardanoToken)?.let {
add(
NotificationUM.Cardano.MinAdaValueCharged(
tokenName = cryptoCurrency.name,
minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals),
),
)
}
else -> return
}
}
private fun MutableList<NotificationUM>.addCardanoTransactionValidationError(
error: BlockchainSdkError.Cardano,
sendingCurrency: CryptoCurrency,
dustValue: BigDecimal?,
) {
when (error) {
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name))
}
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
when (sendingCurrency) {
is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin
is CryptoCurrency.Token -> {
NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)
}
}.let(::add)
}
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
-> {
dustValue?.let {
add(
NotificationUM.Error.MinimumAmountError(
amount = it.parseBigDecimal(sendingCurrency.decimals),
),
)
}
}
}
}
private fun MutableList<NotificationUM>.addKoinosTransactionValidationError(
error: BlockchainSdkError.Koinos,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
when (error) {
is BlockchainSdkError.Koinos.InsufficientBalance -> {
add(NotificationUM.Koinos.InsufficientBalance)
}
is BlockchainSdkError.Koinos.InsufficientMana -> {
add(
NotificationUM.Koinos.InsufficientRecoverableMana(
mana = error.manaBalance ?: BigDecimal.ZERO,
maxMana = error.maxMana ?: BigDecimal.ZERO,
),
)
}
is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> {
add(
NotificationUM.Koinos.ManaExceedsBalance(
availableKoinForTransfer = error.availableKoinForTransfer,
onReduceClick = {
onReduceClick(
error.availableKoinForTransfer,
NotificationUM.Koinos.InsufficientRecoverableMana::class.java,
)
},
),
)
}
else -> {}
}
}
private fun checkDustLimits(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
dustValue: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
): Boolean {
val change = when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
balance - (feeAmount + receivedAmount)
}
is CryptoCurrency.Token -> {
val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
balance - feeAmount
}
}
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.common.ui.tokens
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
return when (val unavailabilityReason = this) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_staking_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_send,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_sell,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
}
}
is ScenarioUnavailabilityReason.EmptyBalance -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_send,
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_sell,
)
}
}
is ScenarioUnavailabilityReason.BuyUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_buy_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotExchangeable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_not_exchangeable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotSupportedBySellService -> {
resourceReference(
id = R.string.token_button_unavailability_reason_sell_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
ScenarioUnavailabilityReason.Unreachable -> {
resourceReference(
id = R.string.token_button_unavailability_generic_description,
)
}
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
)
ScenarioUnavailabilityReason.None -> {
throw IllegalArgumentException("The unavailability reason must be other than None")
}
}
}

View file

@ -0,0 +1,193 @@
package com.tangem.common.ui.tokens
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
/**
* Token item state converter from [CryptoCurrencyStatus] to [TokenItemState]
*
* @property appCurrency app currency
* @property titleStateProvider title state provider
* @property subtitleStateProvider subtitle state provider
* @property onItemClick callback is invoked when item is clicked
* @property onItemLongClick callback is invoked when item is long clicked
*/
class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = Companion::createTitleState,
private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = {
createSubtitleState(it, appCurrency)
},
private val onItemClick: (CryptoCurrencyStatus) -> Unit,
private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
return when (value.value) {
is CryptoCurrencyStatus.Loading -> value.mapToLoadingState()
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> value.mapToTokenItemState()
is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState()
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> value.mapToUnreachableTokenItemState()
}
}
private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading {
return TokenItemState.Loading(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = titleStateProvider(this) as TokenItemState.TitleState.Content,
subtitleState = requireNotNull(subtitleStateProvider(this)),
)
}
private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content {
return TokenItemState.Content(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = titleStateProvider(this),
subtitleState = requireNotNull(subtitleStateProvider(this)),
fiatAmountState = TokenItemState.FiatAmountState.Content(
text = getFormattedFiatAmount(),
hasStaked = !getStakedBalance().isZero(),
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
},
)
}
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount?.plus(getStakedBalance()) ?: return DASH_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero()
val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)
}
private fun CryptoCurrencyStatus.getStakedBalance() =
(value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero()
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState(): TokenItemState.Unreachable {
return TokenItemState.Unreachable(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
},
)
}
private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState(): TokenItemState.NoAddress {
return TokenItemState.NoAddress(
id = currency.id.value,
iconState = iconStateConverter.convert(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemLongClick = onItemLongClick?.let {
{ it(this) }
},
)
}
private companion object {
fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState {
return when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
TokenItemState.TitleState.Content(text = currencyStatus.currency.name)
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.TitleState.Content(
text = currencyStatus.currency.name,
hasPending = value.hasCurrentNetworkTransactions,
)
}
}
}
fun createSubtitleState(
currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
): TokenItemState.SubtitleState? {
return when (currencyStatus.value) {
is CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> currencyStatus.getCryptoPriceState(appCurrency)
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> null
}
}
private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState {
val fiatRate = value.fiatRate
val priceChange = value.priceChange
return if (fiatRate != null && priceChange != null) {
TokenItemState.SubtitleState.CryptoPriceContent(
price = fiatRate.getFormattedCryptoPrice(appCurrency),
priceChangePercent = BigDecimalFormatter.formatPercent(
percent = priceChange,
useAbsoluteValue = true,
),
type = priceChange.getPriceChangeType(),
)
} else {
TokenItemState.SubtitleState.Unknown
}
}
private fun BigDecimal.getFormattedCryptoPrice(appCurrency: AppCurrency): String {
return BigDecimalFormatter.formatFiatAmountUncapped(
fiatAmount = this,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
private fun BigDecimal.getPriceChangeType(): PriceChangeType {
return PriceChangeConverter.fromBigDecimal(value = this)
}
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.tokens
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
/**
* Text view for token price.
*
* @param price Price of the token.
* @param priceChangeType Type of the price change.
*/
@Composable
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
val growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember(generalColor) { Animatable(generalColor) }
var animationSkipped by remember { mutableStateOf(false) }
LaunchedEffect(price) {
if (animationSkipped.not()) {
animationSkipped = true
return@LaunchedEffect
}
if (priceChangeType != null) {
val nextColor = when (priceChangeType) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@LaunchedEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
}
Text(
modifier = modifier,
text = price,
color = color.value,
maxLines = 1,
style = TangemTheme.typography.body2,
overflow = TextOverflow.Visible,
)
}

View file

@ -0,0 +1,213 @@
package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.common.ui.R
import com.tangem.core.ui.coil.RotationTransformation
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.persistentListOf
@Composable
fun UserWalletItem(
state: UserWalletItemUM,
modifier: Modifier = Modifier,
blockColors: CardColors = TangemBlockCardColors,
) {
BlockCard(
modifier = modifier,
colors = blockColors,
onClick = state.onClick,
enabled = state.isEnabled,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(imageUrl = state.imageUrl)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
information = state.information,
)
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> {}
UserWalletItemUM.EndIcon.Arrow -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
UserWalletItemUM.EndIcon.Checkmark -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
val list = persistentListOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
endIcon = UserWalletItemUM.EndIcon.Arrow,
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
)
Column(
Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
list.fastForEach { userWalletItemUM ->
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
state = userWalletItemUM,
)
}
}
}
}
private fun getInformation(cardCount: Int, totalBalance: String): TextReference {
val t1 = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val divider = stringReference(value = "")
val t2 = stringReference(totalBalance)
return TextReference.Combined(wrappedList(t1, divider, t2))
}

View file

@ -0,0 +1,103 @@
package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.converter.Converter
/**
* Converter from [UserWallet] to [UserWalletItemUM]
*
* @property onClick lambda be invoked when item is clicked
* @property appCurrency selected app currency
* @property balance wallet balance
* @property isLoading wallet loading state
* @property isBalanceHidden wallet balance is hidden
*
[REDACTED_AUTHOR]
*/
class UserWalletItemUMConverter(
private val onClick: (UserWalletId) -> Unit,
private val appCurrency: AppCurrency? = null,
private val balance: TotalFiatBalance? = null,
private val isLoading: Boolean = true,
private val isBalanceHidden: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
) : Converter<UserWallet, UserWalletItemUM> {
override fun convert(value: UserWallet): UserWalletItemUM {
return with(value) {
UserWalletItemUM(
id = walletId,
name = stringReference(name),
information = getInfo(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
isLoading = isLoading,
),
imageUrl = artworkUrl,
isEnabled = !isLocked,
endIcon = endIcon,
onClick = { onClick(value.walletId) },
)
}
}
private fun UserWallet.getInfo(
appCurrency: AppCurrency?,
balance: TotalFiatBalance?,
isBalanceHidden: Boolean,
isLoading: Boolean,
): TextReference {
val dividerRef = stringReference(value = " $DOT ")
val cardCount = getCardsCount() ?: 1
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return when {
isBalanceHidden -> combinedReference(cardCountRef, dividerRef, TextReference.STARS)
isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked))
isLoading -> cardCountRef
else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef)
}
}
private fun getBalanceInfo(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
cardCountRef: TextReference,
dividerRef: TextReference,
): TextReference {
val amount = when (balance) {
is TotalFiatBalance.Loaded -> balance.amount
is TotalFiatBalance.Failed,
is TotalFiatBalance.Loading,
null,
-> null
}
return if (amount != null && appCurrency != null) {
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val amountRef = stringReference(formattedAmount)
combinedReference(cardCountRef, dividerRef, amountRef)
} else {
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import javax.annotation.concurrent.Immutable
@Immutable
data class UserWalletItemUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val imageUrl: String,
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
) {
enum class EndIcon {
None,
Arrow,
Checkmark,
}
}

View file

@ -81,6 +81,12 @@ sealed class AnalyticsParam {
override val feeType: FeeType, override val feeType: FeeType,
) : TxSentFrom("Swap"), TxData ) : TxSentFrom("Swap"), TxData
data class Staking(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Staking"), TxData
data class Approve( data class Approve(
override val blockchain: String, override val blockchain: String,
override val token: String, override val token: String,
@ -131,7 +137,7 @@ sealed class AnalyticsParam {
companion object Key { companion object Key {
const val BLOCKCHAIN = "blockchain" const val BLOCKCHAIN = "blockchain"
const val TOKEN = "Token" const val TOKEN_PARAM = "Token"
const val SOURCE = "Source" const val SOURCE = "Source"
const val BALANCE = "Balance" const val BALANCE = "Balance"
const val STATE = "State" const val STATE = "State"
@ -152,5 +158,12 @@ sealed class AnalyticsParam {
const val VALIDATION = "Validation" const val VALIDATION = "Validation"
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host"
const val BLOCKCHAIN_SELECTED_HOST = "selected_host" const val BLOCKCHAIN_SELECTED_HOST = "selected_host"
// region swap
const val TOKEN_CATEGORY = "Token"
const val STATUS = "Status"
const val PROVIDER = "Provider"
const val PLACE = "Place"
//
} }
} }

View file

@ -55,7 +55,7 @@ sealed class Basic(
this[AnalyticsParam.SOURCE] = sentFrom.value this[AnalyticsParam.SOURCE] = sentFrom.value
if (sentFrom is AnalyticsParam.TxData) { if (sentFrom is AnalyticsParam.TxData) {
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
this[AnalyticsParam.TOKEN] = sentFrom.token this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token
this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value
} }
if (sentFrom is AnalyticsParam.TxSentFrom.Approve) { if (sentFrom is AnalyticsParam.TxSentFrom.Approve) {

View file

@ -0,0 +1,8 @@
package com.tangem.core.analytics
interface AppInstanceIdProvider {
suspend fun getAppInstanceId(): String?
fun getAppInstanceIdSync(): String?
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.analytics
class DummyAppInstanceIdProvider : AppInstanceIdProvider {
override suspend fun getAppInstanceId(): String? {
return null
}
override fun getAppInstanceIdSync(): String? {
return null
}
}

View file

@ -8,8 +8,11 @@ import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardClaimingDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardClaimingDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.RewardTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.error.AccessDeniedErrorTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO
@ -26,6 +29,7 @@ object UnknownEnumMoshiAdapter {
fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
val map = mapOf( val map = mapOf(
// valid response enums
BalanceTypeDTO::class.java to BalanceTypeDTO.UNKNOWN, BalanceTypeDTO::class.java to BalanceTypeDTO.UNKNOWN,
NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN, NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN,
RewardClaimingDTO::class.java to RewardClaimingDTO.UNKNOWN, RewardClaimingDTO::class.java to RewardClaimingDTO.UNKNOWN,
@ -35,6 +39,10 @@ fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN,
StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN,
StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN,
ValidatorStatusDTO::class.java to ValidatorStatusDTO.UNKNOWN,
// error enums
AccessDeniedErrorTypeDTO::class.java to AccessDeniedErrorTypeDTO.UNKNOWN,
StakeKitErrorMessageDTO::class.java to StakeKitErrorMessageDTO.UNKNOWN,
) )
return apply { return apply {

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.markets.models.response package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TokenMarketChartResponse( data class TokenMarketChartResponse(
@Json(name = "prices") @Json(name = "prices")
val prices: Map<Long, BigDecimal>, val prices: Map<Long, BigDecimal>,

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