Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-17 12:51:39 +03:00
commit aa4fa2b3eb
732 changed files with 24719 additions and 7348 deletions

View file

@ -18,6 +18,9 @@ android {
jniLibs {
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 io.github.kakaocup.compose.node.element.ComposeScreen
class WalletScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletScreen>(
class MainTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<MainTestScreen>(
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.text.KButton
class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StoriesScreen>(
class StoriesTestScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StoriesTestScreen>(
semanticsProvider = semanticsProvider,
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
import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerScreen
import com.tangem.screens.StoriesScreen
import com.tangem.screens.WalletScreen
import com.tangem.scenarios.OpenMainScreenScenario
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test
@HiltAndroidTest
@ -14,26 +11,6 @@ class MainScreenTest : BaseTestCase() {
@Test
fun goToMain() =
setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(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()
}
}
scenario(OpenMainScreenScenario(composeTestRule))
}
}

View file

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

View file

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

View file

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

@ -1 +1 @@
Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef
Subproject commit 328c13826f7e52ba783db1ba39de5b17b1b79bc5

View file

@ -708,6 +708,16 @@
"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.config.ConfigManager
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.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -76,7 +76,7 @@ interface ApplicationEntryPoint {
fun getBalanceHidingRepository(): BalanceHidingRepository
fun getUserTokensStore(): UserTokensStore
fun getAppPreferencesStore(): AppPreferencesStore
fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase

View file

@ -583,8 +583,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun sendStakingUnsubmittedHashes() {
lifecycleScope.launch {
sendUnsubmittedHashesUseCase.invoke()
.onRight { Timber.d("Submitting hashes succeeded") }
.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.models.Config
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.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val balanceHidingRepository: BalanceHidingRepository
get() = entryPoint.getBalanceHidingRepository()
private val userTokensStore: UserTokensStore
get() = entryPoint.getUserTokensStore()
private val appPreferencesStore: AppPreferencesStore
get() = entryPoint.getAppPreferencesStore()
val getAppThemeModeUseCase: GetAppThemeModeUseCase
get() = entryPoint.getGetAppThemeModeUseCase()
@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
}
derivationsFinder = DerivationsFinder(
newTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
dispatchers = AppCoroutineDispatcherProvider(),
)
appStateHolder.mainStore = store

View file

@ -51,6 +51,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
source = state.dialog.source,
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.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
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.sdk.DefaultSessionViewDelegate
import com.tangem.sdk.extensions.*
import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider
import com.tangem.sdk.nfc.NfcManager
import com.tangem.sdk.storage.create
import com.tangem.tap.foregroundActivityObserver
@ -98,9 +99,11 @@ internal class DefaultCardSdkProvider @Inject constructor(
val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity)
viewDelegate.sdkConfig = config
val androidNfcAvailabilityProvider = AndroidNfcAvailabilityProvider(activity)
val sdk = TangemSdk(
reader = nfcManager.reader,
viewDelegate = viewDelegate,
nfcAvailabilityProvider = androidNfcAvailabilityProvider,
secureStorage = secureStorage,
authenticationManager = authenticationManager,
keystoreManager = keystoreManager,

View file

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

View file

@ -92,4 +92,12 @@ internal object CardDomainModule {
fun provideNetworkHasDerivationUseCase(): 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
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.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,4 +22,84 @@ internal object ManageTokensDomainModule {
fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase {
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
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.GetTokenQuotesUseCase
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.markets.*
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.Provides
import dagger.hilt.InstallIn
@ -37,7 +38,29 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
fun provideTokenFullQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenFullQuotesUseCase {
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)
}
@Provides
@Singleton
fun provideShouldShowMarketsTooltipUseCase(
settingsRepository: SettingsRepository,
): ShouldShowMarketsTooltipUseCase {
return ShouldShowMarketsTooltipUseCase(settingsRepository = settingsRepository)
}
@Provides
@Singleton
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.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakingTransactionHashRepository
import dagger.Module
import dagger.Provides
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
@Singleton
fun provideInitializeStakingProcessUseCase(
@ -112,11 +101,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideSubmitHashUseCase(
stakingRepository: StakingRepository,
stakingTransactionHashRepository: StakingTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver,
): SubmitHashUseCase {
return SubmitHashUseCase(
stakingRepository = stakingRepository,
stakingTransactionHashRepository = stakingTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -124,11 +113,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideSaveUnsubmittedHashUseCase(
stakingRepository: StakingRepository,
stakingTransactionHashRepository: StakingTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver,
): SaveUnsubmittedHashUseCase {
return SaveUnsubmittedHashUseCase(
stakingRepository = stakingRepository,
stakingTransactionHashRepository = stakingTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -136,23 +125,11 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideSendUnsubmittedHashesUseCase(
stakingRepository: StakingRepository,
stakingTransactionHashRepository: StakingTransactionHashRepository,
stakingErrorResolver: StakingErrorResolver,
): SendUnsubmittedHashesUseCase {
return SendUnsubmittedHashesUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideIsStakeMoreAvailableUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsStakeMoreAvailableUseCase {
return IsStakeMoreAvailableUseCase(
stakingRepository = stakingRepository,
stakingTransactionHashRepository = stakingTransactionHashRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@ -180,4 +157,16 @@ internal object StakingDomainModule {
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.walletmanager.WalletManagersFacade
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.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -38,8 +39,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchTokenListUseCase {
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository)
return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
}
@Provides
@ -73,8 +75,8 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository)
): GetNodlTokenListUseCase {
return GetNodlTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository)
}
@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
@Singleton
fun provideGetCurrencyWarningsUseCase(
@ -158,8 +176,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository)
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
}
@Provides
@ -213,6 +232,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
stakingFeatureToggles: StakingFeatureToggles,
marketsFeatureToggles: MarketsFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
@ -224,6 +244,7 @@ internal object TokensDomainModule {
networksRepository = networksRepository,
stakingRepository = stakingRepository,
stakingFeatureToggles = stakingFeatureToggles,
marketsFeatureToggles = marketsFeatureToggles,
dispatchers = dispatchers,
)
}
@ -416,7 +437,7 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase {
return CheckHasLinkedTokensUseCase(currenciesRepository)
fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase {
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
@Singleton
fun provideAssociateAssetUseCase(

View file

@ -1,5 +1,7 @@
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.card.EllipticCurve
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.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore
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.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -30,6 +35,25 @@ internal class DefaultDerivationsRepository(
) : DerivationsRepository {
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")
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
@ -38,7 +62,7 @@ internal class DefaultDerivationsRepository(
}
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.find(currencies)
.findByNetworks(networks)
.ifEmpty {
Timber.d("Nothing to derive")
return
@ -47,6 +71,26 @@ internal class DefaultDerivationsRepository(
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 {
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.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.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.operations.derivation.ExtendedPublicKeysMap
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] */
fun find(currencies: List<CryptoCurrency>): Derivations {
return currencies.map { it.network }.let(::findByNetworks)
}
fun findByNetworks(networks: List<Network>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
currencies
networks
.mapToNewDerivations()
.forEach { data ->
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)
return mapNotNull { currency ->
val blockchain = Blockchain.fromId(id = currency.network.id.value)
return mapNotNull { network ->
val blockchain = Blockchain.fromId(id = network.id.value)
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(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
network: Network,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val publicKey = wallet.publicKey.toMapKey()
val derivationCandidates = currency
val derivationCandidates = network
.getDerivationCandidates(curve)
.ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey)
@ -68,13 +73,13 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
return publicKey to derivationCandidates
}
private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = network.id.value)
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = this.id.value)
return buildList {
add(blockchain.getDerivationPath(curve = curve))
add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates))
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
}
.filterNotNull()
.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)) {
currency.network.derivationPath.value?.let(::DerivationPath)
network.derivationPath.value?.let(::DerivationPath)
} else {
null
}
}
private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? {
return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
return if (this == Blockchain.Cardano) {
network.derivationPath.value?.let {
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
}
} 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.TangemSdkError
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.Wallet2MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -67,6 +68,7 @@ object MockProvider {
return when (productType) {
ProductType.Wallet -> WalletMockContent
ProductType.Wallet2 -> Wallet2MockContent
ProductType.Note -> NoteMockContent
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
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.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.backup.PrimaryCard
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
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 Wallet2MockContent : MockContent {
override val scanResponse: ScanResponse
get() = TODO("Not yet implemented")
private val primaryCard = PrimaryCard(
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
get() = TODO("Not yet implemented")
override val cardDto = CardDTO(
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
get() = TODO("Not yet implemented")
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet2,
walletData = null,
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val extendedPublicKey: ExtendedPublicKey
get() = TODO("Not yet implemented")
override val derivationTaskResponse = DerivationTaskResponse(
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
get() = TODO("Not yet implemented")
override val extendedPublicKey = ExtendedPublicKey(
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
get() = TODO("Not yet implemented")
override val successResponse = SuccessResponse(cardId = "AF10000000981426")
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
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),
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(
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),
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.blockchainsdk.utils.fromNetworkId
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.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO
@ -22,7 +25,7 @@ internal data class BlockchainToDerive(
// FIXME: May be move to DI, currently unnecessary
internal class DerivationsFinder(
private val newTokensStore: UserTokensStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -64,7 +67,9 @@ internal class DerivationsFinder(
}
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
val responseTokens = newTokensStore.getSyncOrNull(userWalletId)
val responseTokens = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
?.tokens
?: return hashSetOf()

View file

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

View file

@ -43,7 +43,7 @@ import kotlinx.coroutines.launch
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current),
)
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.datasource.CollectionPreviewParameterProvider
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.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(),
warning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
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.PreviewParameter
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.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_cancel),
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.common.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.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
@ -159,6 +160,8 @@ internal class CardSettingsViewModel @Inject constructor(
}
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)))
cardSettingsInteractor.clear()
@ -173,13 +176,7 @@ internal class CardSettingsViewModel @Inject constructor(
userWalletId = userWalletId,
cardId = card.cardId,
isActiveBackupStatus = card.backupStatus?.isActive == true,
backupCardsCount = when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
CardDTO.BackupStatus.NoBackup,
null,
-> 0
},
backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
),
)
}

View file

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

View file

@ -7,18 +7,11 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
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.components.SystemBarsIconsDisposable
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.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.HomeState
import com.tangem.tap.store
@ -32,9 +25,6 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var homeFeatureToggles: HomeFeatureToggles
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private val viewModel by viewModels<HomeViewModel>()
@ -77,31 +67,9 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
private fun ScreenContent() {
StoriesScreen(
homeState = homeState,
onScanButtonClick = {
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
viewModel.onScanClick()
} 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)
}
},
onScanButtonClick = viewModel::onScanClick,
onShopButtonClick = viewModel::onShopClick,
onSearchTokensClick = viewModel::onSearchClick,
)
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.home
import androidx.lifecycle.ViewModel
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.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -45,6 +47,8 @@ internal class HomeViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
fun onScanClick() {
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
scanCard()
@ -54,14 +58,16 @@ internal class HomeViewModel @Inject constructor(
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
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() {
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
}
private fun scanCard() {
@ -79,7 +85,7 @@ internal class HomeViewModel @Inject constructor(
}
},
onFailure = {
Timber.e(it, "Unable to scan card")
tangemErrorHandler.onErrorReceived(error = it)
delay(HIDE_PROGRESS_DELAY)
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 ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data object GoToShop : HomeAction()
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
}

View file

@ -1,7 +1,5 @@
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.doOnResult
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.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.dispatchNavigationAction
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.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.scope
import com.tangem.tap.store
@ -61,21 +60,6 @@ private fun handleHomeAction(action: Action) {
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() {
viewModelScope.launch(dispatchers.main) {
fetchStakingTokensUseCase()
fetchStakingTokensUseCase(true)
.onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
.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) {
when (previousStep) {
TwinCardsStep.None -> {
TwinCardsStep.None, TwinCardsStep.Welcome -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(false) {
twinsWidget.toActivate(false)
twinsWidget.leapfrogWidget.unfold(true) {
twinsWidget.toActivate(true)
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) {
twinsWidget.toActivate(false)
switchToCard(state.cardNumber, true) {
twinsWidget.toActivate(true)
}
}
else -> {}

View file

@ -23,7 +23,9 @@ sealed class OnboardingWalletAction : Action {
data object ResumeBackup : 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()
}

View file

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

View file

@ -21,8 +21,20 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetArtworkUrl -> {
state.copy(cardArtworkUri = action.artworkUri)
is OnboardingWalletAction.SetPrimaryCardArtworkUrl -> {
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)
else -> state

View file

@ -12,7 +12,7 @@ data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(),
val cardArtworkUri: Uri? = null,
val walletImages: WalletImages = WalletImages(),
val showConfetti: Boolean = false,
val isRingOnboarding: Boolean = false,
) : StateType {
@ -49,6 +49,12 @@ data class OnboardingWalletState(
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(
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.Basic
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.datasource.utils.isNullOrEmpty
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
@ -186,30 +187,27 @@ class OnboardingWalletFragment :
when {
state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
state.cardArtworkUri?.let {
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)
}
updateWalletImagesState(state.walletImages)
}
else -> {
if (state.isRingOnboarding) {
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
updateWalletImagesState(state.walletImages)
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) {
view.load(uri) {
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 com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Composable
@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) {
BasicDialog(
title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_enable),
onClick = dialog.onEnroll,
),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
onClick = 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.AppRouter
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.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
@ -48,12 +47,6 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(state, 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.SendCoin -> handleNewSendCoin(action = action)
}
@ -141,22 +134,6 @@ object TradeCryptoMiddleware {
)?.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) {
handleNewSend(
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.tooling.preview.Preview
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.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R
@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) {
},
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) {
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) {
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
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.managetokens.ManageTokensToggles
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.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
@ -50,6 +51,7 @@ internal class ChildFactory @Inject constructor(
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter,
@ -126,13 +128,7 @@ internal class ChildFactory @Inject constructor(
if (manageTokensToggles.isFeatureEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params(
mode = if (route.readOnlyContent) {
ManageTokensComponent.Mode.READ_ONLY
} else {
ManageTokensComponent.Mode.MANAGE
},
),
params = ManageTokensComponent.Params(route.userWalletId),
componentFactory = manageTokensComponentFactory,
)
} else {
@ -191,6 +187,23 @@ internal class ChildFactory @Inject constructor(
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
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
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.getNetworkDerivationPath
import com.tangem.data.common.currency.getNetworkStandardType
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
@ -40,11 +44,23 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
}
private fun createCoin(blockchain: Blockchain): CryptoCurrency {
return factory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)!!
val network = Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
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
@ -68,6 +84,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = true,
canHandleTokens = true,
),
name = "NEVER-MIND",
symbol = "NEVER-MIND",

View file

@ -19,6 +19,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.markets.models)
implementation(projects.domain.appCurrency.models)
/* Libs - Other */
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.entity.SerializableIntent
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.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
@ -177,8 +179,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class ManageTokens(
val readOnlyContent: Boolean,
) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams {
val userWalletId: UserWalletId? = null,
) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@ -217,12 +219,14 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Swap(
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())
companion object {
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(
val userWalletId: UserWalletId,
) : 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
.drawBehind {
state.markerFraction
state.chartColor
state.markerHighlightRightSide
},
chart = chart,
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.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
/** Deps */
implementation(deps.kotlin.immutable.collections)
@ -27,9 +28,14 @@ dependencies {
implementation(projects.core.utils)
/** Project - Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.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) {
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
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
@ -20,24 +19,28 @@ import com.tangem.core.ui.res.TangemThemePreview
/**
* Amount screen with field
* @param amountState amount state
* @param isBalanceHiding flag hidden balances
* @param isBalanceHidden flag hidden balances
* @param clickIntents amount screen clicks
*/
@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
// Do not put fillMaxSize() in here
LazyColumn(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
amountField(amountState = amountState, isBalanceHiding = isBalanceHiding)
amountField(amountState = amountState, isBalanceHidden = isBalanceHidden)
buttons(
segmentedButtonConfig = amountState.segmentedButtonConfig,
clickIntents = clickIntents,
@ -57,7 +60,7 @@ private fun SendAmountContentPreview(
TangemThemePreview {
AmountScreenContent(
amountState = amountState,
isBalanceHiding = false,
isBalanceHidden = false,
clickIntents = AmountScreenClickIntentsStub,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters.field
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
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.getFiatValue
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.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -61,7 +63,8 @@ class AmountFieldChangeTransformer(
value = cryptoValue,
fiatValue = fiatValue,
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),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(
@ -81,6 +84,10 @@ class AmountFieldChangeTransformer(
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
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.ui.text.input.ImeAction
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.models.AmountFieldModel
import com.tangem.core.ui.extensions.TextReference
@ -62,7 +61,8 @@ class AmountFieldConverter(
cryptoAmount = cryptoAmount,
fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()),
isError = false,
error = TextReference.Res(R.string.send_validation_amount_exceeds_balance),
isWarning = false,
error = TextReference.EMPTY,
isFiatUnavailable = fiatRate == null,
isValuePasted = false,
onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss,

View file

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

View file

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

View file

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

View file

@ -14,15 +14,15 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.amountScreen.models.AmountState
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.res.TangemTheme
import com.tangem.utils.StringsSigns.STARS
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
internal fun LazyListScope.amountField(
amountState: AmountState.Data,
isBalanceHiding: Boolean,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
item(key = AMOUNT_FIELD_KEY) {
@ -41,7 +41,7 @@ internal fun LazyListScope.amountField(
.padding(top = TangemTheme.dimens.spacing14),
)
val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference()
val balance = amountState.walletBalance.orMaskWithStars(isBalanceHidden).resolveReference()
AnimatedContent(
targetState = balance,
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.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -59,7 +58,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
BasicDialog(
message = content.data.dialogText.resolveReference(),
title = stringResource(id = R.string.common_approve),
confirmButton = DialogButton { isPermissionAlertShow = false },
confirmButton = DialogButtonUM { isPermissionAlertShow = false },
onDismissDialog = {},
)
}
@ -166,7 +165,7 @@ private fun AmountItem(
.clickable(
enabled = onChangeApproveType != null,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
indication = ripple(),
onClick = { isExpandSelector = true },
),
) {

View file

@ -2,27 +2,32 @@ package com.tangem.common.ui.navigationButtons
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.material3.Text
import androidx.compose.runtime.*
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.graphics.vector.rememberVectorPainter
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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
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.TangemButtonIconPosition
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.isNullOrEmpty
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -30,39 +35,34 @@ import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@Composable
fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifier = Modifier) {
fun NavigationButtonsBlock(
buttonState: NavigationButtonsState,
modifier: Modifier = Modifier,
footerText: TextReference? = null,
) {
val state = buttonState as? NavigationButtonsState.Data
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth(),
) {
InfoText(footerText)
ExtraButtons(state?.extraButtons, state?.txUrl)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
PreviousButton(state?.prevButton)
PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
}
SecondaryButton(state?.secondaryButton)
}
}
@Composable
private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
val wrappedButton by rememberNavigationButton(primaryButton)
AnimatedContent(
targetState = primaryButton,
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())
}
},
targetState = wrappedButton,
transitionSpec = { navigationButtonsTransition() },
contentAlignment = Alignment.Center,
label = "Animate show primary button",
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
private fun PreviousButton(prevButton: NavigationButton?) {
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
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -10,9 +10,9 @@ sealed class NavigationButtonsState {
data class Data(
val primaryButton: NavigationButton,
val prevButton: NavigationButton?,
val secondaryButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>,
val txUrl: String? = null,
val onTextClick: (String) -> Unit,
) : 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(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
@ -60,8 +52,8 @@ internal object NavigationButtonsPreview {
val allButtons = NavigationButtonsState.Data(
primaryButton = finished,
prevButton = prev,
secondaryButton = next,
extraButtons = extraButtons,
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,
) : TxSentFrom("Swap"), TxData
data class Staking(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Staking"), TxData
data class Approve(
override val blockchain: String,
override val token: String,
@ -131,7 +137,7 @@ sealed class AnalyticsParam {
companion object Key {
const val BLOCKCHAIN = "blockchain"
const val TOKEN = "Token"
const val TOKEN_PARAM = "Token"
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val STATE = "State"
@ -152,5 +158,12 @@ sealed class AnalyticsParam {
const val VALIDATION = "Validation"
const val BLOCKCHAIN_EXCEPTION_HOST = "exception_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
if (sentFrom is AnalyticsParam.TxData) {
this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain
this[AnalyticsParam.TOKEN] = sentFrom.token
this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token
this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value
}
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.RewardScheduleDTO
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.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.StakingTransactionTypeDTO
@ -26,6 +29,7 @@ object UnknownEnumMoshiAdapter {
fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
val map = mapOf(
// valid response enums
BalanceTypeDTO::class.java to BalanceTypeDTO.UNKNOWN,
NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN,
RewardClaimingDTO::class.java to RewardClaimingDTO.UNKNOWN,
@ -35,6 +39,10 @@ fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN,
StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.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 {

View file

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

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