diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 68ed0730c5..2c48609883 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,6 +80,7 @@ configurations.androidTestImplementation { dependencies { implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.core) implementation(projects.domain.card) @@ -144,6 +145,7 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.libs.tangemSdkApi) + implementation(projects.data.account) implementation(projects.data.appCurrency) implementation(projects.data.appTheme) implementation(projects.data.balanceHiding) @@ -226,13 +228,21 @@ dependencies { implementation(projects.features.usedesk.impl) implementation(projects.features.hotWallet.api) implementation(projects.features.hotWallet.impl) + implementation(projects.features.kyc.api) //TODO disable for release because of the permissions - // implementation(projects.features.kyc.api) // implementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) implementation(projects.features.createWalletSelection.impl) + implementation(projects.features.home.api) + implementation(projects.features.home.impl) + implementation(projects.features.account.api) + implementation(projects.features.account.impl) + implementation(projects.features.tangempay.details.api) + implementation(projects.features.tangempay.details.impl) + implementation(projects.features.tangempay.main.api) + implementation(projects.features.tangempay.main.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -255,13 +265,11 @@ dependencies { /** Compose libraries */ implementation(deps.compose.constraintLayout) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.navigation.hilt) implementation(deps.compose.shimmer) implementation(deps.compose.ui) @@ -346,7 +354,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index d42aeddab4..823d7df27c 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -15,8 +15,11 @@ import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestRule @@ -40,6 +43,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var apiConfigsManager: ApiConfigsManager + @Inject + lateinit var appPreferencesStore: AppPreferencesStore + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( @@ -73,6 +79,14 @@ abstract class BaseTestCase : TestCase( additionalAfterSection: () -> Unit = {}, ) = before { hiltRule.inject() + runBlocking { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.set( + key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, + value = false + ) + } + } apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) Intents.init() diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt new file mode 100644 index 0000000000..00cfc3afe3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -0,0 +1,7 @@ +package com.tangem.common.constants + +object TestConstants { + const val TOTAL_BALANCE = "$3,299.18" + + const val WAIT_UNTIL_TIMEOUT = 20_000L +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt new file mode 100644 index 0000000000..72637225d9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt @@ -0,0 +1,17 @@ +package com.tangem.common.extensions + +import com.tangem.common.BaseTestCase + +fun BaseTestCase.swipeUp( + startHeightRatio: Float = 0.8f, + endHeightRatio: Float = 0.03f, + steps: Int = 15 +) { + device.uiDevice.swipe( + device.uiDevice.displayWidth / 2, + (device.uiDevice.displayHeight * startHeightRatio).toInt(), + device.uiDevice.displayWidth / 2, + (device.uiDevice.displayHeight * endHeightRatio).toInt(), + steps + ) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt index 0b0f1a32d0..a8d1a2c038 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt @@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule { ApiConfig.ID.TangemTech, ApiConfig.ID.Express, ApiConfig.ID.TangemPay, + ApiConfig.ID.StakeKit, ) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt new file mode 100644 index 0000000000..fe4c30d9a6 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt @@ -0,0 +1,119 @@ +package com.tangem.common.utils + +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import timber.log.Timber +import java.io.IOException + +/** + * Method uses to set WireMock scenario state + * @param scenarioName Name of the scenario to modify + * @param state The target state to set (must be one of the scenario's possibleStates) + * @param baseUrl WireMock base URL + * @return true if state was set successfully, false otherwise + */ +fun setWireMockScenarioState( + scenarioName: String, + state: String, + baseUrl: String = "[REDACTED_ENV_URL]" +): Boolean { + Timber.i("=== WireMock Scenario Set ===") + Timber.i("Setting scenario '$scenarioName' to state: $state") + val client = OkHttpClient() + val json = """{"state": "$state"}""" + val mediaType = "application/json".toMediaType() + + val request = Request.Builder() + .url("$baseUrl/__admin/scenarios/$scenarioName/state") + .put(json.toRequestBody(mediaType)) + .build() + + return try { + client.newCall(request).execute().use { response -> + val body = response.body?.string() ?: "" + Timber.d("WireMock scenario request URL: ${request.url}") + Timber.d("WireMock scenario request body: $json") + Timber.d("WireMock scenario response: ${response.code} - ${response.message}") + Timber.d("WireMock scenario response body: $body") + response.isSuccessful + } + } catch (e: IOException) { + Timber.e(e, "WireMock scenario error") + false + } +} + +/** + * Method checks accessibility of WireMock + */ +fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { + val client = OkHttpClient() + val request = Request.Builder() + .url("$baseUrl/__admin/scenarios") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + val body = response.body?.string() ?: "" + Timber.d("WireMock status check: ${response.code}") + Timber.d("Available scenarios: $body") + response.isSuccessful + } + } catch (e: IOException) { + Timber.e(e, "WireMock not accessible") + false + } +} + +/** + * Method to reset all WireMock scenarios + */ +fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { + Timber.i("=== WireMock Scenarios Reset ===") + Timber.i("Base URL: $baseUrl") + + val client = OkHttpClient() + val url = "$baseUrl/__admin/scenarios/reset" + Timber.i("Request URL: $url") + + val request = Request.Builder() + .url(url) + .post("".toRequestBody()) + .build() + + return try { + Timber.d("Sending reset request...") + client.newCall(request).execute().use { response -> + Timber.d("Response code: ${response.code}") + Timber.d("Response message: ${response.message}") + val responseBody = response.body?.string() ?: "" + Timber.d("Response body: $responseBody") + + val isSuccessful = response.isSuccessful + Timber.d("Is successful: $isSuccessful") + isSuccessful + } + } catch (e: IOException) { + Timber.e(e, "Exception during reset") + false + } +} + +/** + * Method to reset a specific WireMock scenario to its initial state + * @param scenarioName Name of the scenario to reset + * @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates) + * @param baseUrl WireMock base URL + * @return true if reset was successful, false otherwise + */ +fun resetWireMockScenarioState( + scenarioName: String, + initialState: String = "Started", + baseUrl: String = "[REDACTED_ENV_URL]" +): Boolean { + Timber.i("=== WireMock Scenario Reset ===") + Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState") + return setWireMockScenarioState(scenarioName, initialState, baseUrl) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt new file mode 100644 index 0000000000..4193f3ab8b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt @@ -0,0 +1,101 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags +import com.tangem.core.ui.test.NotificationTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag + +class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val topBarMoreButton: KNode = child { + hasTestTag(TopAppBarTestTags.MORE_BUTTON) + useUnmergedTree = true + } + + val topBarCloseButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val errorNotificationTitle: KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText(getResourceString(R.string.common_error)) + useUnmergedTree = true + } + + val errorNotificationText: KNode = child { + hasTestTag(NotificationTestTags.TEXT) + hasText(getResourceString(R.string.common_unknown_error)) + useUnmergedTree = true + } + + val refreshButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.warning_button_refresh)) + } + + val fiatCurrencyIcon: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON) + useUnmergedTree = true + } + + val expandFiatListButton: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON) + useUnmergedTree = true + } + + val fiatAmountTextField: KNode = child { + hasParent(withTestTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD)) + useUnmergedTree = true + } + + val tokenAmountField: KNode = child { + hasParent(withTestTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT)) + useUnmergedTree = true + } + + val providerLoadingTitle: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE) + } + + val providerLoadingText: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT) + } + + val providerTitle: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE) + useUnmergedTree = true + } + + val providerText: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT) + useUnmergedTree = true + } + + val buyButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_buy)) + } + + val toSBlock: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onBuyTokenDetailsScreen(function: BuyTokenDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt new file mode 100644 index 0000000000..9e453330b3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt @@ -0,0 +1,38 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.BuyTokenFiatListTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode + +class BuyTokenFiatListPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(BuyTokenFiatListTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + fun fiatListItemWithTitle(title: String): KNode { + return lazyList.child { + hasText(title) + } + } +} + +internal fun BaseTestCase.onBuyTokenFiatListBottomSheet(function: BuyTokenFiatListPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt new file mode 100644 index 0000000000..1dbe1d3ba3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt @@ -0,0 +1,54 @@ +package com.tangem.screens + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.test.TokenElementsTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topAppBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_buy)) + useUnmergedTree = true + } + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(BuyTokenScreenTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + @OptIn(ExperimentalTestApi::class) + fun tokenWithTitleAndFiatAmount(tokenTitle: String): KNode { + return lazyList.childWith { + hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + hasText(tokenTitle) + useUnmergedTree = true + }.child { + hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT) + useUnmergedTree = true + } + } +} + +internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 960b371248..5bac035afc 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -3,6 +3,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.DialogTestTags import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen @@ -17,14 +18,19 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : } val cancelButton: KNode = child { - hasTestTag(DialogTestTags.BUTTON) + hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_cancel)) } val hideButton: KNode = child { - hasTestTag(DialogTestTags.BUTTON) + hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.token_details_hide_alert_hide)) } + + val confirmButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_confirm)) + } } internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt index fa12033630..9e43faf303 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt @@ -3,9 +3,12 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.test.DisclaimerScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.features.disclaimer.impl.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen( @@ -13,6 +16,15 @@ class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) viewBuilderAction = { hasTestTag(DisclaimerScreenTestTags.SCREEN_CONTAINER) } ) { + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.disclaimer_title)) + } + + val webView: KNode = child { + hasTestTag(DisclaimerScreenTestTags.WEB_VIEW) + } + val acceptButton: KNode = child { hasTestTag(DisclaimerScreenTestTags.ACCEPT_BUTTON) } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index c9a08a06f8..ce3a3181b3 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -39,6 +39,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(R.string.common_generate_addresses)) } + val buyButton: KNode = child { + hasTestTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON) + hasText(getResourceString(R.string.common_buy)) + } + /** * Find token list item with title and address */ diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt new file mode 100644 index 0000000000..b16bba6546 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt @@ -0,0 +1,84 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.ReferralProgramScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.feature.referral.presentation.R as ReferralPresentationR +import androidx.compose.ui.test.hasText as withText + +class ReferralProgramPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.details_referral_title)) + useUnmergedTree = true + } + + val referTitle: KNode = child { + hasText(getResourceString(R.string.referral_title)) + useUnmergedTree = true + } + + val image: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.IMAGE) + useUnmergedTree = true + } + + val infoForYouText: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT) + useUnmergedTree = true + } + + val infoForYouBlock: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.CONDITION_BLOCK) + hasAnyDescendant( + withText( + getResourceString(ReferralPresentationR.string.referral_point_currencies_title), + substring = true + ) + ) + useUnmergedTree = true + } + + val infoForYourFriendText: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.INFO_FOR_YOUR_FRIEND_TEXT) + useUnmergedTree = true + } + + val infoForYourFriendBlock: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.CONDITION_BLOCK) + hasAnyDescendant( + withText( + getResourceString(ReferralPresentationR.string.referral_point_discount_title), + substring = true + ) + ) + useUnmergedTree = true + } + + val agreementText: KNode = child { + hasText(getResourceString( + ReferralPresentationR.string.referral_tos_not_enroled_prefix) + " " + + getResourceString(ReferralPresentationR.string.common_terms_and_conditions ) + " " + + getResourceString(ReferralPresentationR.string.referral_tos_suffix), + substring = true + ) + useUnmergedTree = true + } + + val participateButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onReferralProgramScreen(function: ReferralProgramPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt new file mode 100644 index 0000000000..c25bad4120 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt @@ -0,0 +1,46 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.ResidenceSettingsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.onramp.impl.R as OnrampImplR + +class ResidenceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.onramp_settings_title)) + useUnmergedTree = true + } + + val topBarCloseButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val residenceButton: KNode = child { + hasText(getResourceString(OnrampImplR.string.onramp_settings_residence)) + useUnmergedTree = true + } + + val countryName: KNode = child { + hasTestTag(ResidenceSettingsScreenTestTags.COUNTRY_NAME) + useUnmergedTree = true + } + + val residenceSettingsDescription: KNode = child { + hasText(getResourceString(OnrampImplR.string.onramp_settings_residence_description)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onResidenceSettingsScreen(function: ResidenceSettingsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt new file mode 100644 index 0000000000..b3a6a9d573 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt @@ -0,0 +1,59 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import com.tangem.features.onramp.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText +import androidx.compose.ui.test.hasTestTag as withTestTag + + +class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(SelectCountryBottomSheetTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + val searchBar: KNode = child { + hasTestTag(SelectCountryBottomSheetTestTags.SEARCH_BAR) + useUnmergedTree = true + } + + fun countryWithNameAndIcon(name: String): KNode { + return lazyList.child { + hasText(name) + hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.COUNTRY_ICON)) + useUnmergedTree = true + } + } + + fun unavailableCountryWithNameAndIcon(name: String): KNode { + return lazyList.child { + hasText(name) + hasAnySibling(withText(getResourceString(R.string.onramp_country_unavailable))) + hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ICON)) + useUnmergedTree = true + } + } +} + +internal fun BaseTestCase.onSelectCountryBottomSheet(function: SelectCountryPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt new file mode 100644 index 0000000000..35a55722df --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt @@ -0,0 +1,42 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_fee_selector_title)) + useUnmergedTree = true + } + + val marketSelectorItem: KNode = child { + hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM) + hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market))) + useUnmergedTree = true + } + + val fastSelectorItem: KNode = child { + hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM) + hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast))) + useUnmergedTree = true + } + + val readMoreTextBlock: KNode = child { + hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt new file mode 100644 index 0000000000..0739883572 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt @@ -0,0 +1,50 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import com.tangem.features.onramp.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText + +class SelectPaymentMethodPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.onramp_pay_with)) + } + + fun paymentMethodWithNameAndIcon(name: String): KNode { + return lazyList.child { + hasAnyDescendant(withText(name)) + hasAnyDescendant(withTestTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON)) + useUnmergedTree = true + } + } +} + +internal fun BaseTestCase.onSelectPaymentMethodBottomSheet(function: SelectPaymentMethodPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt new file mode 100644 index 0000000000..2c8a050019 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt @@ -0,0 +1,91 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags +import com.tangem.features.onramp.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class SelectProviderPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(R.string.onramp_choose_provider_title_hint)) + useUnmergedTree = true + } + + val paymentMethodIcon: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON) + useUnmergedTree = true + } + + val paymentMethodTitle: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME) + useUnmergedTree = true + } + + val paymentMethodName: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME) + useUnmergedTree = true + } + + val paymentMethodExpandButton: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_EXPAND_BUTTON) + useUnmergedTree = true + } + + val availableProviderItem: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM) + useUnmergedTree = true + } + + val availableProviderName: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_NAME) + useUnmergedTree = true + } + + val tokenAmount: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.TOKEN_AMOUNT) + useUnmergedTree = true + } + + val unavailableProviderItem: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_ITEM) + useUnmergedTree = true + } + + val unavailableProviderName: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_NAME) + useUnmergedTree = true + } + + val moreProvidersIcon: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_ICON) + useUnmergedTree = true + } + + val moreProvidersText: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_TEXT) + useUnmergedTree = true + } + + val bestRateLabel: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.BEST_RATE_LABEL) + useUnmergedTree = true + } + + fun availableProviderWithName(name: String, tokenAmount: String, rate: String): KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM) + hasAnyChild(withText(name)) + hasAnyChild(withText(tokenAmount)) + hasAnyChild(withText(rate)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSelectProviderBottomSheet(function: SelectProviderPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt new file mode 100644 index 0000000000..7e118e51b1 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt @@ -0,0 +1,117 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.* +import com.tangem.features.tokendetails.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.staking.impl.R as StakingImplR +import androidx.compose.ui.test.hasTestTag as withTestTag + +class StakingDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) + } + + val stakingTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val bannerImage: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.BANNER_IMAGE) + useUnmergedTree = true + } + + val bannerText: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.BANNER_TEXT) + useUnmergedTree = true + } + + val annualPercentageRate: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_annual_percentage_rate)) + useUnmergedTree = true + } + + val availableBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_available)) + useUnmergedTree = true + + } + + val unbondingPeriodBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_unbonding_period)) + useUnmergedTree = true + } + + val rewardClaimingBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_reward_claiming)) + useUnmergedTree = true + } + + val rewardScheduleBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_reward_schedule)) + useUnmergedTree = true + } + + val rewardsBlock: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK) + useUnmergedTree = true + } + + val rewardsBlockTitle: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK_TITLE) + useUnmergedTree = true + } + + val rewardsBlockText: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK_TEXT) + useUnmergedTree = true + } + + val yourStakesTitle: KNode = child { + hasText(getResourceString(StakingImplR.string.staking_your_stakes)) + useUnmergedTree = true + } + + val activeStakingBlock: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK) + useUnmergedTree = true + } + + val toSText: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.TOS_TEXT) + useUnmergedTree = true + } + + val stakeMoreButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.staking_stake_more)) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingDetailsScreen(function: StakingDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt new file mode 100644 index 0000000000..79b38d7c9c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt @@ -0,0 +1,51 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val primaryAmount: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT) + useUnmergedTree = true + } + + val secondaryAmount: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + val validatorBlock: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK) + useUnmergedTree = true + } + + val networkFeeBlock: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt new file mode 100644 index 0000000000..f71c9ad210 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt @@ -0,0 +1,78 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.send.v2.impl.R as SendR +import androidx.compose.ui.test.hasTestTag as withTestTag + +class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER) + } + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val amountContainerTitle: KNode = child { + hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE) + useUnmergedTree = true + } + + val amountContainerText: KNode = child { + hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT) + useUnmergedTree = true + } + + val amountInputTextField: KNode = child { + hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD) + useUnmergedTree = true + } + + val secondaryAmount: KNode = child { + hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + val currencyButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON)) + useUnmergedTree = true + } + + val fiatButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON)) + useUnmergedTree = true + } + + val maxButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.MAX_BUTTON) + useUnmergedTree = true + } + + val previousButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON) + useUnmergedTree = true + } + + val nextButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(SendR.string.common_next)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt new file mode 100644 index 0000000000..e48a981112 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt @@ -0,0 +1,20 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SwapStoriesScreenTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val closeButton: KNode = child { + hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt new file mode 100644 index 0000000000..dd53bc1dc5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -0,0 +1,77 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.* +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag + +class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_swap)) + useUnmergedTree = true + } + + val closeButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + + val textInput: KNode = child { + hasParent(withTestTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD)) + useUnmergedTree = true + } + + val networkFeeBlock: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK) + useUnmergedTree = true + } + + val receiveAmountShimmer: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER) + } + + val swapTokensOnscreenButton: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON) + } + + val receiveAmount: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD) + useUnmergedTree = true + } + + val providersBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK) + useUnmergedTree = true + } + + val errorNotificationTitle: KNode = child { + hasTestTag(NotificationTestTags.TITLE) + useUnmergedTree = true + } + + val errorNotificationText: KNode = child { + hasTestTag(NotificationTestTags.TEXT) + useUnmergedTree = true + } + + val refreshButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.warning_button_refresh)) + } + + val swapButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_swap)) + } + +} + +internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index 2f8eae1c51..128a24ad03 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -1,11 +1,19 @@ package com.tangem.screens +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.TokenDetailsScreenTestTags +import com.tangem.features.tokendetails.impl.R +import com.tangem.core.ui.utils.LazyListItemPositionSemantics import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -13,6 +21,96 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide val screenContainer: KNode = child { hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) } + + val availableStakingBlock: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK) + useUnmergedTree = true + } + + val stakingBlock: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_BLOCK) + useUnmergedTree = true + } + + val availableStakingBlockTitle: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE) + useUnmergedTree = true + } + + val availableStakingBlockText: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT) + useUnmergedTree = true + } + + val availableStakingBlockCurrencyIcon: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + + val stakingFiatAmount: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT) + useUnmergedTree = true + } + + val stakingDot: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT) + useUnmergedTree = true + } + + val stakingTokenAmount: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT) + useUnmergedTree = true + } + + val stakingChevronIcon: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON) + useUnmergedTree = true + } + + val stakingTitle: KNode = child { + hasText(getResourceString(R.string.staking_native)) + } + + val title: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE) + } + + private val horizontalActionChips = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + @OptIn(ExperimentalTestApi::class) + val swapButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_swap)) + } + + @OptIn(ExperimentalTestApi::class) + val sellButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_sell)) + } + + @OptIn(ExperimentalTestApi::class) + val buyButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_buy)) + } + } internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt new file mode 100644 index 0000000000..f553c1cbe0 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -0,0 +1,499 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class BuyTokenTest : BaseTestCase() { + + @AllureId("3478") + @DisplayName("Onramp: error in providers loading") + @Test + fun errorInProvidersLoadingTest() { + val scenarioName = "payment_methods" + val tokenTitle = "Bitcoin" + val balance = TOTAL_BALANCE + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + step("Setup WireMock scenario '$scenarioName' for 'Error' state") { + setWireMockScenarioState(scenarioName, "Error") + } + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Assert error notification title is displayed") { + onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() } + } + step("Assert error notification text is displayed") { + onBuyTokenDetailsScreen { errorNotificationText.assertIsDisplayed() } + } + step("Assert 'Refresh' button is displayed and clickable") { + onBuyTokenDetailsScreen { refreshButton.clickWithAssertion() } + } + } + } + + @AllureId("2565") + @DisplayName("Onramp: validate currency selector") + @Test + fun validateCurrencySelectorTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + val popularFiatsTitle = "Popular Fiats" + val otherCurrenciesTitle = "Other currencies" + val australianDollar = "AUD" + val fiatAmount = "1" + val tokenAmount = "POLĀ 488.24938338" + val scenarioName = "payment_methods" + + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert 'Provider loading block' is displayed") { + onBuyTokenDetailsScreen { + providerLoadingTitle.assertIsDisplayed() + providerLoadingText.assertIsDisplayed() + } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Assert token amount = '$tokenAmount'") { + onBuyTokenDetailsScreen { + tokenAmountField.assertTextContains(tokenAmount) + } + } + step("Fiat currency icon is displayed") { + onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() } + } + step("Click on 'Expand fiat list' button") { + onBuyTokenDetailsScreen { expandFiatListButton.clickWithAssertion() } + } + step("Assert '$popularFiatsTitle' is displayed") { + onBuyTokenFiatListBottomSheet { + fiatListItemWithTitle(popularFiatsTitle).assertIsDisplayed() + } + } + step("Assert '$otherCurrenciesTitle' is displayed") { + onBuyTokenFiatListBottomSheet { + fiatListItemWithTitle(otherCurrenciesTitle).assertIsDisplayed() + } + } + step("Click on fiat with title: '$australianDollar'") { + onBuyTokenFiatListBottomSheet { + fiatListItemWithTitle(australianDollar).performClick() + } + } + step("Assert new fiat currency: '$australianDollar' is displayed") { + onBuyTokenDetailsScreen { + fiatAmountTextField.assertTextContains(australianDollar + fiatAmount) + } + } + step("Assert token amount = '$tokenAmount'") { + onBuyTokenDetailsScreen { + tokenAmountField.assertTextContains(tokenAmount) + } + } + } + } + + @AllureId("2566") + @DisplayName("Onramp: validate 'Buy token' screen") + @Test + fun validateBuyTokenScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + val euro = "EUR" + val fiatAmount = "1" + val tokenAmount = "POLĀ 488.24938338" + val scenarioName = "payment_methods" + + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Assert 'Buy Token' title is displayed") { + onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") } + } + step("Assert 'More button' in top bar is displayed") { + onBuyTokenDetailsScreen { topBarMoreButton.assertIsDisplayed() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) } + } + step("Assert 'Provider loading block' is displayed") { + onBuyTokenDetailsScreen { + providerLoadingTitle.assertIsDisplayed() + providerLoadingText.assertIsDisplayed() + } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Assert token amount = '$tokenAmount'") { + onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) } + } + step("Assert 'ToS' block is displayed") { + onBuyTokenDetailsScreen { toSBlock.assertIsDisplayed()} + } + step("Assert 'Buy' button is displayed") { + onBuyTokenDetailsScreen { buyButton.assertIsDisplayed()} + } + step("Assert 'Close' button in top bar is displayed") { + onBuyTokenDetailsScreen { topBarCloseButton.assertIsDisplayed() } + } + } + } + + @AllureId("2563") + @DisplayName("Onramp: validate 'Residence' settings screen") + @Test + fun validateResidenceSettingsScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + val country = "Albania" + val unavailableCountry = "Lebanon" + val scenarioName = "payment_methods" + + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Assert 'Buy $tokenTitle' title is displayed") { + onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") } + } + step("Click 'More' button in tab bar") { + onBuyTokenDetailsScreen { topBarMoreButton.clickWithAssertion() } + } + step("Assert 'Residence Settings' screen top bar title is displayed") { + onResidenceSettingsScreen { topBarTitle.assertIsDisplayed() } + } + step("Assert 'Residence Settings' screen top bar 'Close' button is displayed") { + onResidenceSettingsScreen { topBarCloseButton.assertIsDisplayed() } + } + step("Assert 'Residence' button is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { residenceButton.assertIsDisplayed() } + } + step("Assert country name is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { countryName.assertIsDisplayed() } + } + step("Assert residence settings description is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { residenceSettingsDescription.assertIsDisplayed() } + } + step("Click 'Residence button'") { + onResidenceSettingsScreen { residenceButton.clickWithAssertion() } + } + step("Assert 'Search bar' is displayed") { + onSelectCountryBottomSheet { searchBar.assertIsDisplayed() } + } + step("Type unavailable country name: '$unavailableCountry' in 'Search bar'") { + onSelectCountryBottomSheet { searchBar.performTextReplacement(unavailableCountry) } + } + step("Unavailable country: '$unavailableCountry' is displayed") { + onSelectCountryBottomSheet { unavailableCountryWithNameAndIcon(unavailableCountry).assertIsDisplayed() } + } + step("Type country name: '$country' in 'Search bar'") { + onSelectCountryBottomSheet { searchBar.performTextReplacement(country) } + } + step("Available country: '$country' is displayed") { + onSelectCountryBottomSheet { countryWithNameAndIcon(country).assertIsDisplayed() } + } + step("Click on country: '$country'") { + onSelectCountryBottomSheet { countryWithNameAndIcon(country).clickWithAssertion() } + } + step("Assert country: '$country' is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { countryName.assertTextContains(country) } + } + } + } + + @AllureId("2570") + @DisplayName("Onramp: validate 'Select provider' bottom sheet") + @Test + fun validateProvidersScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + val paymentMethod = "Card" + val fiatAmount = "1" + val providerNameMercuryo = "Mercuryo" + val providerNameSimplex = "Simplex" + val tokenAmount = "POLĀ 488.24938338" + val bestRate = "Best rate" + val rate = "-0.00%" + val scenarioName = "payment_methods" + + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Open 'Select Provider' bottom sheet") { + onBuyTokenDetailsScreen { providerTitle.performClick() } + } + step("Assert available provider name is displayed") { + onSelectProviderBottomSheet { + flakySafely(WAIT_UNTIL_TIMEOUT) { + availableProviderItem.assertIsDisplayed() + } + } + } + step("Click on 'Expand payment methods' button") { + onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() } + } + step("Click on payment method: '$paymentMethod'") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(paymentMethod).clickWithAssertion() } + } + step("Assert 'Select Provider' bottom sheet title is displayed") { + onSelectProviderBottomSheet { title.assertIsDisplayed() } + } + step("Assert payment method icon is displayed") { + onSelectProviderBottomSheet { paymentMethodIcon.assertIsDisplayed() } + } + step("Assert payment method title is displayed") { + onSelectProviderBottomSheet { paymentMethodTitle.assertIsDisplayed() } + } + step("Assert payment method name is displayed") { + onSelectProviderBottomSheet { paymentMethodName.assertIsDisplayed() } + } + step("Assert provider with name: '$providerNameMercuryo' and rate: '$bestRate' is displayed") { + onSelectProviderBottomSheet { + availableProviderWithName(providerNameMercuryo, tokenAmount, bestRate).assertIsDisplayed() + } + } + step("Assert provider with name: '$providerNameSimplex' and rate: '$rate' is displayed") { + onSelectProviderBottomSheet { + availableProviderWithName(providerNameSimplex, tokenAmount, rate).assertIsDisplayed() + } + } + step("Assert 'More providers' icon is displayed") { + onSelectProviderBottomSheet { moreProvidersIcon.assertIsDisplayed() } + } + step("Assert 'More providers' text is displayed") { + onSelectProviderBottomSheet { moreProvidersText.assertIsDisplayed() } + } + step("Assert 'Best rate' label is displayed") { + onSelectProviderBottomSheet { bestRateLabel.assertIsDisplayed() } + } + } + } + + @AllureId("3479") + @DisplayName("Onramp: validate 'Select payment method' bottom sheet") + @Test + fun validatePaymentMethodScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + val card = "Card" + val googlePay = "Google Pay" + val invoiceRevolutPay = "Invoice Revolut Pay" + val sepa = "Sepa" + val fiatAmount = "1" + val scenarioName = "payment_methods" + + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Open 'Select Provider' bottom sheet") { + onBuyTokenDetailsScreen { providerTitle.performClick() } + } + step("Click on 'Expand payment methods' button") { + onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() } + } + step("Assert 'Select Payment Method' bottom sheet title is displayed") { + onSelectPaymentMethodBottomSheet { title.assertIsDisplayed() } + } + step("Assert payment method: '$card' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(card).assertIsDisplayed() } + } + step("Assert payment method: '$googlePay' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(googlePay).assertIsDisplayed() } + } + step("Assert payment method: '$invoiceRevolutPay' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(invoiceRevolutPay).assertIsDisplayed() } + } + step("Assert payment method: '$sepa' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(sepa).assertIsDisplayed() } + } + step("Press 'Back' button") { + onSelectPaymentMethodBottomSheet { device.uiDevice.pressBack() } + } + step("Assert 'Select Provider' bottom sheet title is displayed") { + onSelectProviderBottomSheet { title.assertIsDisplayed() } + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 1242a1285e..df318a9456 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -5,9 +5,12 @@ import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.onDetailsScreen +import com.tangem.screens.onReferralProgramScreen import com.tangem.screens.onTopBar import com.tangem.screens.onWalletSettingsScreen import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName import org.junit.Test @HiltAndroidTest @@ -61,7 +64,7 @@ class DetailsTest : BaseTestCase() { } } - @Test + // @Test fun wallet2DetailsTest() = setupHooks().run { scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2)) @@ -153,4 +156,48 @@ class DetailsTest : BaseTestCase() { } } } + + @AllureId("3647") + @DisplayName("Referral program: validate screen") + @Test + fun validateReferralProgramScreenTest() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule)) + step("Open wallet details") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Open 'Wallet settings' screen") { + onDetailsScreen { walletNameButton.clickWithAssertion() } + } + step("Click on 'Referral program' button ") { + onWalletSettingsScreen { referralProgramButton.clickWithAssertion() } + } + step("Assert 'Referral program' screen title is displayed") { + onReferralProgramScreen { title.assertIsDisplayed() } + } + step("Assert 'Referral program' screen image is displayed") { + onReferralProgramScreen { image.assertIsDisplayed() } + } + step("Assert 'Referral program' screen refer title is displayed") { + onReferralProgramScreen { referTitle.assertIsDisplayed() } + } + step("Assert info for you title is displayed") { + onReferralProgramScreen { infoForYouText.assertIsDisplayed() } + } + step("Assert info for you text is displayed") { + onReferralProgramScreen { infoForYouBlock.assertIsDisplayed() } + } + step("Assert info for your friend title is displayed") { + onReferralProgramScreen { infoForYourFriendText.assertIsDisplayed() } + } + step("Assert info for your friend text is displayed") { + onReferralProgramScreen { infoForYourFriendBlock.assertIsDisplayed() } + } + step("Assert agreement text is displayed") { + onReferralProgramScreen { agreementText.assertIsDisplayed() } + } + step("Assert 'Participate' button is displayed") { + onReferralProgramScreen { participateButton.assertIsDisplayed() } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt index dd0dee7e9e..706bd497d5 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.* @@ -17,7 +18,7 @@ class HideTokenTest : BaseTestCase() { @Test fun hideWalletTokenByHideButtonTest() { val tokenTitle = "Polygon" - val balance = "<$0.01" + val balance = TOTAL_BALANCE setupHooks().run { step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 3856e30249..76d8e34a7a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -2,7 +2,9 @@ package com.tangem.tests import androidx.compose.ui.test.onAllNodesWithText import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeUp import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.onMainScreen import com.tangem.screens.onOrganizeTokensScreen @@ -27,6 +29,10 @@ class OrganizeTokensTest : BaseTestCase() { step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -48,6 +54,10 @@ class OrganizeTokensTest : BaseTestCase() { step("Assert tokens were grouped on 'Main screen'") { onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -79,18 +89,26 @@ class OrganizeTokensTest : BaseTestCase() { setupHooks().run { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" + val balance = TOTAL_BALANCE step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } step("Check positions of tokens on 'Main Screen'") { onMainScreen { tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -123,6 +141,10 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -154,12 +176,17 @@ class OrganizeTokensTest : BaseTestCase() { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" val polygonTitle = "Polygon" + val polExMaticTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } step("Check positions of tokens on 'Main Screen'") { onMainScreen { tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed() @@ -167,6 +194,10 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -175,6 +206,7 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed() + tokenWithTitleAndPosition(polExMaticTitle, 4).assertIsDisplayed() } } step("Click 'By Balance' button") { @@ -185,8 +217,9 @@ class OrganizeTokensTest : BaseTestCase() { step("Check positions of tokens by balance on 'Organize tokens' screen") { onOrganizeTokensScreen { tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() - tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() - tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed() + tokenWithTitleAndPosition(polExMaticTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed() + tokenWithTitleAndPosition(bitcoinTitle, 4).assertIsDisplayed() } } step("Click 'Apply' button") { @@ -195,8 +228,9 @@ class OrganizeTokensTest : BaseTestCase() { step("Check positions of tokens by balance on 'Organize tokens' screen") { onMainScreen { tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed() - tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed() - tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(polExMaticTitle, 1).assertIsDisplayed() + tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt new file mode 100644 index 0000000000..083478f475 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt @@ -0,0 +1,379 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class StakingTest : BaseTestCase() { + + @AllureId("3558") + @DisplayName("Staking: validate staking block on 'Token details' screen") + @Test + fun validateStakingBlockTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Staked" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Staking block' is displayed") { + onTokenDetailsScreen { stakingBlock.assertIsDisplayed() } + } + step("Assert 'Staking title' is displayed") { + onTokenDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert 'Staking fiat amount' is displayed") { + onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() } + } + step("Assert 'Staking dot' is displayed") { + onTokenDetailsScreen { stakingDot.assertIsDisplayed() } + } + step("Assert 'Staking token amount' is displayed") { + onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() } + } + step("Assert 'Staking block chevron icon' is displayed") { + onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() } + } + } + } + + @AllureId("3550") + @DisplayName("Staking: validate staking more screens") + @Test + fun validateStakingMoreScreensTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Staked" + val stakingAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Click on 'Staking block'") { + onTokenDetailsScreen { stakingBlock.clickWithAssertion() } + } + step("Assert 'Title' is displayed") { + onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert 'Annual percentage rate' is displayed") { + onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() } + } + step("Assert 'Available' block is displayed") { + onStakingDetailsScreen { availableBlock.assertIsDisplayed() } + } + step("Assert 'Unbonding Period' block is displayed") { + onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() } + } + step("Assert 'Reward claiming' block is displayed") { + onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() } + } + step("Assert 'Reward schedule' block is displayed") { + onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() } + } + step("Assert 'Rewards block' is displayed") { + onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() } + } + step("Assert 'Rewards block' title is displayed") { + onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() } + } + step("Assert 'Rewards block' text is displayed") { + onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() } + } + step("Assert 'Active staking block' is displayed") { + onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() } + } + step("Assert 'Your stakes' title is displayed") { + onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() } + } + step("Assert 'ToS' text is displayed") { + onStakingDetailsScreen { toSText.assertIsDisplayed() } + } + step("Assert 'Stake more' button is displayed") { + onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() } + } + step("Click 'Stake more' button") { + onStakingDetailsScreen { stakeMoreButton.performClick() } + } + step("Assert 'Send' screen is displayed") { + onStakingSendScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Send' screen title is displayed") { + onStakingSendScreen { title.assertIsDisplayed() } + } + step("Assert amount container title is displayed") { + onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + } + step("Assert amount container text is displayed") { + onStakingSendScreen { amountContainerText.assertIsDisplayed() } + } + step("Assert input text field is displayed") { + onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + } + step("Type '$stakingAmount' in input text field") { + onStakingSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(stakingAmount) + } + } + step("Assert input text field has value: '$stakingAmount'") { + onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert 'Max' button is displayed") { + onStakingSendScreen { maxButton.assertIsDisplayed() } + } + step("Assert previous button is displayed") { + onStakingSendScreen { previousButton.assertIsDisplayed() } + } + step("Assert 'Next' button is displayed") { + onStakingSendScreen { nextButton.assertIsDisplayed() } + } + step("Click on 'Next' button") { + onStakingSendScreen { nextButton.performClick() } + } + step("Assert 'Send details' screen title is displayed") { + onStakingSendDetailsScreen { title.assertIsDisplayed() } + } + step("Assert primary amount is displayed") { + onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + } + step("Assert 'Validator' block is displayed") { + onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + } + step("Assert 'Network Fee' block is displayed") { + onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + } + } + } + + @AllureId("3548") + @DisplayName("Staking: validate staking screens") + @Test + fun validateStakingScreensTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Started" + val stakingAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Available staking block' is displayed") { + onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() } + } + step("Assert 'Available staking block' title is displayed") { + onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() } + } + step("Assert 'Available staking block' text is displayed") { + onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() } + } + step("Assert 'Available staking block' currency icon is displayed") { + onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() } + } + step("Click on 'Stake' button") { + onTokenDetailsScreen { stakeButton.clickWithAssertion() } + } + step("Assert 'Title' is displayed") { + onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert banner image is displayed") { + onStakingDetailsScreen { bannerImage.assertIsDisplayed() } + } + step("Assert banner text is displayed") { + onStakingDetailsScreen { bannerText.assertIsDisplayed() } + } + step("Assert 'Annual percentage rate' is displayed") { + onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() } + } + step("Assert 'Available' block is displayed") { + onStakingDetailsScreen { availableBlock.assertIsDisplayed() } + } + step("Assert 'Unbonding Period' block is displayed") { + onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() } + } + step("Assert 'Reward claiming' block is displayed") { + onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() } + } + step("Assert 'Reward schedule' block is displayed") { + onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() } + } + step("Assert 'ToS' text is displayed") { + onStakingDetailsScreen { toSText.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingDetailsScreen { stakeButton.assertIsDisplayed() } + } + step("Click 'Stake' button") { + onStakingDetailsScreen { stakeButton.performClick() } + } + step("Assert 'Send' screen is displayed") { + onStakingSendScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Send' screen title is displayed") { + onStakingSendScreen { title.assertIsDisplayed() } + } + step("Assert amount container title is displayed") { + onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + } + step("Assert amount container text is displayed") { + onStakingSendScreen { amountContainerText.assertIsDisplayed() } + } + step("Assert input text field is displayed") { + onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + } + step("Type '$stakingAmount' in input text field") { + onStakingSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(stakingAmount) + } + } + step("Assert input text field has value: '$stakingAmount'") { + onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert 'Max' button is displayed") { + onStakingSendScreen { maxButton.assertIsDisplayed() } + } + step("Assert previous button is displayed") { + onStakingSendScreen { previousButton.assertIsDisplayed() } + } + step("Assert 'Next' button is displayed") { + onStakingSendScreen { nextButton.assertIsDisplayed() } + } + step("Click on 'Next' button") { + onStakingSendScreen { nextButton.performClick() } + } + step("Assert 'Send details' screen title is displayed") { + onStakingSendDetailsScreen { title.assertIsDisplayed() } + } + step("Assert primary amount is displayed") { + onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + } + step("Assert 'Validator' block is displayed") { + onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + } + step("Assert 'Network Fee' block is displayed") { + onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index c17f7fc08d..d87e1be98c 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -5,17 +5,16 @@ import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.screens.onDisclaimerScreen import com.tangem.screens.onStoriesScreen -import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.intent.KIntent -import org.junit.Test @HiltAndroidTest class StoriesTest : BaseTestCase() { - @Test + // @Test fun clickOnOrderButtonTest() = setupHooks().run { + val buyWalletUrl = "https://buy.tangem.com/" onDisclaimerScreen { step("Click on 'Accept' button") { acceptButton.clickWithAssertion() @@ -28,7 +27,7 @@ class StoriesTest : BaseTestCase() { step("Assert: browser opened") { val expectedIntent = KIntent { hasAction(ACTION_VIEW) - hasData { toString().startsWith(NEW_BUY_WALLET_URL) } + hasData { toString().startsWith(buyWalletUrl) } } expectedIntent.intended() device.uiDevice.pressBack() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt new file mode 100644 index 0000000000..638218b875 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt @@ -0,0 +1,240 @@ +package com.tangem.tests + +import androidx.compose.ui.test.hasText +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapTokenTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("3546") + @DisplayName("Swap: network fee") + @Test + fun networkFeeTest() { + val inputAmount = "100" + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert 'Close' button is displayed") { + onSwapTokenScreen { closeButton.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert receive amount is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + receiveAmount.assertIsDisplayed() + } + } + } + step("Input swap amount = '$inputAmount'") { + composeTestRule.waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert input amount = '$inputAmount'") { + onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } + } + step("Assert 'Providers' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + providersBlock.assertIsDisplayed() + } + } + } + step("Assert 'Network fee' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + networkFeeBlock.assertIsDisplayed() + } + } + } + step("Assert receive amount is not equal to '0'") { + onSwapTokenScreen { receiveAmount.assert(!hasText("0")) } + } + } + } + + @AllureId("3549") + @DisplayName("Swap: network error test") + @Test + fun networkErrorSwapTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert error notification title is displayed") { + onSwapTokenScreen { errorNotificationTitle.assertIsDisplayed() } + } + step("Assert error notification text is displayed") { + onSwapTokenScreen { errorNotificationText.assertIsDisplayed() } + } + step("Assert 'Refresh' button is displayed") { + onSwapTokenScreen { refreshButton.assertIsDisplayed() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("3546") + @DisplayName("Swap: change network fee") + @Test + fun changeNetworkFeeTest() { + val inputAmount = "100" + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert receive amount is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + receiveAmount.assertIsDisplayed() + } + } + } + step("Input swap amount = '$inputAmount'") { + composeTestRule.waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert input amount = '$inputAmount'") { + onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } + } + step("Click on 'Network fee' block") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + networkFeeBlock.clickWithAssertion() + } + } + } + step("Assert 'Select fee' bottom sheet title is displayed") { + onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() } + } + step("Assert 'Market' item is displayed") { + onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() } + } + step("Assert 'Fast' item is displayed") { + onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() } + } + step("Assert 'Read more' text block is displayed") { + onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() } + } + step("Click on 'Fast' item") { + onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() } + } + step("Assert 'Network fee' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + networkFeeBlock.assertIsDisplayed() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt new file mode 100644 index 0000000000..7f729f1c3c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt @@ -0,0 +1,106 @@ +package com.tangem.tests + +import androidx.test.InstrumentationRegistry.getTargetContext +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeUp +import com.tangem.screens.onDisclaimerScreen +import com.tangem.screens.onStoriesScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class TermsOfServiceTest : BaseTestCase() { + + @AllureId("3573") + @DisplayName("ToS: success acceptance") + @Test + fun validateTermsOfServiceScreenTest() { + setupHooks().run { + val tosUrl = "https://tangem.com/tangem_tos.html" + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { title.assertIsDisplayed() } + } + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { webView.assertIsDisplayed() } + } + step("Verify WebView loads correct URL") { + onDisclaimerScreen { + webView.assertContentDescriptionContains(value = tosUrl, substring = true) + } + } + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Assert 'Stories' screen is opened") { + onStoriesScreen { + scanButton.assertIsDisplayed() + orderButton.assertIsDisplayed()} + } + } + } + + @AllureId("3574") + @DisplayName("ToS: accept after app restart") + @Test + fun acceptTermsOfServiceAfterAppRestart() { + val packageName = getTargetContext().packageName + setupHooks().run { + val tosUrl = "https://tangem.com/tangem_tos.html" + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { title.assertIsDisplayed() } + } + step("Assert WebView of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { webView.assertIsDisplayed() } + } + step("Verify WebView loads correct URL") { + onDisclaimerScreen { + webView.assertContentDescriptionContains(value = tosUrl, substring = true) + } + } + step("'Accept' button is displayed") { + onDisclaimerScreen { acceptButton.assertIsDisplayed() } + } + step("Open recent apps") { + device.uiDevice.pressRecentApps() + } + step("Stop app by swipe") { + swipeUp(startHeightRatio = 0.5f) + } + step("Launch app") { + device.apps.launch(packageName) + } + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { title.assertIsDisplayed() } + } + step("Assert WebView of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { webView.assertIsDisplayed() } + } + step("Verify WebView loads correct URL") { + onDisclaimerScreen { + webView.assertContentDescriptionContains(value = tosUrl, substring = true) + } + } + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Open recent apps") { + device.uiDevice.pressRecentApps() + } + step("Stop app by swipe") { + swipeUp(startHeightRatio = 0.5f) + } + step("Launch app") { + device.apps.launch(packageName) + } + step("Assert 'Stories' screen is opened") { + onStoriesScreen { + scanButton.assertIsDisplayed() + orderButton.assertIsDisplayed()} + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 3ac868e93f..7d225a195e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 +Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ed9d07c577..aa3d4f2a9a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.hot.sdk.TangemHotSdk import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles @@ -142,4 +145,10 @@ interface ApplicationEntryPoint { fun getApiConfigsManager(): ApiConfigsManager fun getUserTokensResponseStore(): UserTokensResponseStore + + fun getUserWalletsListRepository(): UserWalletsListRepository + + fun getTangemHotSdk(): TangemHotSdk + + fun getHotWalletFeatureToggles(): HotWalletFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index e51fc573ea..f6b3c93141 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -7,6 +7,8 @@ import androidx.work.WorkerParameters import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedInject import timber.log.Timber @@ -17,13 +19,22 @@ class LockTimerWorker @AssistedInject constructor( @Assisted params: WorkerParameters, private val settingsRepository: SettingsRepository, private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { Timber.i("onStart job") - val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() - userWalletsListManagerLockable.lock() - settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.lockAllWallets() + .onRight { + settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + } + } else { + val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() + userWalletsListManagerLockable.lock() + settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + } Timber.i("onStart job complete") return Result.success() } diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 57d5d8c6ca..e3d0a18067 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -10,6 +10,8 @@ import com.tangem.common.routing.AppRoute import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.common.extensions.dispatchNavigationAction import kotlinx.coroutines.CoroutineScope @@ -25,6 +27,8 @@ internal class LockUserWalletsTimer( private val settingsRepository: SettingsRepository, private val duration: Duration = with(Duration) { 5.minutes }, private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val coroutineScope: CoroutineScope, ) : LifecycleOwner by context as LifecycleOwner, DefaultLifecycleObserver { @@ -108,20 +112,33 @@ internal class LockUserWalletsTimer( delay(duration) - val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch + if (hotWalletFeatureToggles.isHotWalletEnabled) { + val userWallets = userWalletsListRepository.userWalletsSync() + if (userWallets.isNotEmpty()) { + userWalletsListRepository.lockAllWallets() + .onLeft { + start() + } + .onRight { + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } + } + } else { + val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch - if (userWalletsListManager.hasUserWallets) { - val currentTime = System.currentTimeMillis() + if (userWalletsListManager.hasUserWallets) { + val currentTime = System.currentTimeMillis() - Timber.i( - """ + Timber.i( + """ Finished |- Millis passed: ${currentTime - startTime} - """.trimIndent(), - ) + """.trimIndent(), + ) - userWalletsListManager.lock() - store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + userWalletsListManager.lock() + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 1947cee1b8..9cb6b6cd62 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle +import android.view.KeyEvent import android.view.MotionEvent import android.view.WindowManager import androidx.activity.SystemBarStyle @@ -39,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase @@ -47,7 +49,10 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper @@ -175,13 +180,31 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var testerMenuLauncher: TesterMenuLauncher + @Inject + internal lateinit var intentProcessor: IntentProcessor + + @Inject + internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler + + @Inject + internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler + + @Inject + internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler + + @Inject + internal lateinit var userWalletsListRepository: UserWalletsListRepository + + @Inject + internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles + + @Inject + internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow - // TODO: fixme: inject through DI - private val intentProcessor: IntentProcessor = IntentProcessor() - private val dialogManager = DialogManager() private val onActivityResultCallbacks = mutableListOf() @@ -231,7 +254,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { lifecycle.addObserver(defaultDeviceFlipDetector) if (BuildConfig.TESTER_MENU_ENABLED) { - lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver) + lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver) } } @@ -261,6 +284,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { settingsRepository = settingsRepository, userWalletsListManager = userWalletsListManager, coroutineScope = mainScope, + userWalletsListRepository = userWalletsListRepository, + hotWalletFeatureToggles = hotWalletFeatureToggles, ) initIntentHandlers() @@ -343,12 +368,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun initIntentHandlers() { - val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } - intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler)) - intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) + intentProcessor.addHandler(onPushClickedIntentHandler) if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { - intentProcessor.addHandler(WalletConnectLinkIntentHandler()) + intentProcessor.addHandler(walletConnectLinkIntentHandler) } } @@ -409,11 +432,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { override fun dispatchTouchEvent(event: MotionEvent): Boolean { val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler) - return if (result) super.dispatchTouchEvent(event) else false } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + return if (BuildConfig.TESTER_MENU_ENABLED) { + testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) + } else { + super.dispatchKeyEvent(event) + } + } + private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { + // TODO refactor this method to return a route instead of navigating directly + if (hotWalletFeatureToggles.isHotWalletEnabled) { + navigateToInitialScreenIfNeededNew(intentWhichStartedActivity) + return + } + val backStack = appRouterConfig.stack ?: emptyList() // TODO move inital navigation to navigation component ([REDACTED_JIRA]) val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial } @@ -433,10 +469,73 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } + @Deprecated("Refactor this method to return a route instead of navigating directly") + private fun navigateToInitialScreenIfNeededNew(intentWhichStartedActivity: Intent?) { + lifecycleScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) + if (userWallets.isEmpty()) { + val shouldShowTos = !cardRepository.isTangemTOSAccepted() + + val route = if (shouldShowTos) { + AppRoute.Disclaimer(isTosAccepted = false) + } else { + AppRoute.Home(launchMode = launchMode) + } + + store.dispatchNavigationAction { replaceAll(route) } + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = false, + ) + } else { + if (userWallets.any { it.isLocked }) { + store.dispatchNavigationAction { + replaceAll( + AppRoute.Welcome( + launchMode = launchMode, + intent = intentWhichStartedActivity?.let(::SerializableIntent), + ), + ) + } + } else { + store.dispatchNavigationAction { + replaceAll(AppRoute.Wallet) + } + } + + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = true, + ) + } + + if (intent != null) { + handleDeepLink(intent = intent, isFromOnNewIntent = false) + } + + viewModel.checkForUnfinishedBackup() + } + } + private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { - if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { + val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) + + // Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs + if (tangemPayFeatureToggles.isTangemPayEnabled) { store.dispatchNavigationAction { - replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) + replaceAll(AppRoute.TangemPayDetails) + } + } else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { + store.dispatchNavigationAction { + replaceAll( + AppRoute.Welcome( + launchMode = launchMode, + intent = intentWhichStartedActivity?.let(::SerializableIntent), + ), + ) } intentProcessor.handleIntent( intent = intentWhichStartedActivity, @@ -450,7 +549,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { val route = if (shouldShowTos) { AppRoute.Disclaimer(isTosAccepted = false) } else { - AppRoute.Home + AppRoute.Home(launchMode = launchMode) } store.dispatchNavigationAction { replaceAll(route) } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ef784f8347..09c556cae1 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val userTokensResponseStore: UserTokensResponseStore get() = entryPoint.getUserTokensResponseStore() + private val userWalletsListRepository + get() = entryPoint.getUserWalletsListRepository() + + private val tangemHotSdk + get() = entryPoint.getTangemHotSdk() + + private val hotWalletFeatureToggles + get() = entryPoint.getHotWalletFeatureToggles() + // endregion private val appScope = MainScope() @@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat uiMessageSender = uiMessageSender, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, userTokensResponseStore = userTokensResponseStore, + userWalletsListRepository = userWalletsListRepository, + tangemHotSdk = tangemHotSdk, + hotWalletFeatureToggles = hotWalletFeatureToggles, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt deleted file mode 100644 index b5ce0042ae..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.core.analytics.models.AnalyticsEvent - -/** -[REDACTED_AUTHOR] - */ -sealed class IntroductionProcess( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Introduction Process", event, params) { - - class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") - class ButtonTokensList : IntroductionProcess("Button - Tokens List") - class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") - class ButtonScanCard : IntroductionProcess("Button - Scan Card") - class ButtonRequestSupport : IntroductionProcess("Button - Request Support") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt deleted file mode 100644 index 4076de8685..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.tap.common.extensions.filterNotNull - -/** -[REDACTED_AUTHOR] - */ -sealed class Shop( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Shop", event, params) { - - class ScreenOpened : Shop("Shop Screen Opened") - - class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop( - event = "Purchased", - params = mapOf( - "SKU" to sku, - "Count" to count, - "Amount" to amount, - "Coupon Code" to couponCode, - ).filterNotNull(), - ) - - class Redirected(partnerName: String?) : Shop( - event = "Redirected", - params = partnerName?.let { mapOf("Partner" to partnerName) } ?: mapOf(), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 654dbe07ca..805090e6c0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -2,13 +2,13 @@ package com.tangem.tap.common.analytics.paramsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.extensions.inject import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt deleted file mode 100644 index 04240d7b55..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun Dp.toPx(): Float { - val currentDp = this - return with(LocalDensity.current) { currentDp.toPx() } -} - -fun DpSize.halfHeight(): Dp = this.height / 2 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt deleted file mode 100644 index dfabb2cddf..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.tangem.sdk.extensions.pxToDp - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun Painter.dpSize(): DpSize = DpSize( - intrinsicSize.width.pxToDp().dp, - intrinsicSize.height.pxToDp().dp, -) - -@Composable -private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index f2e7d342ef..e09ba24e78 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -24,7 +24,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) { fun Analytics.setContext(userWallet: UserWallet) { setUserId(userWallet.walletId.stringValue) - // TODO add product type for hot ([REDACTED_TASK_KEY]) + // TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics) if (userWallet is UserWallet.Cold) { addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse)) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Int.kt b/app/src/main/java/com/tangem/tap/common/extensions/Int.kt deleted file mode 100644 index 555dc252bd..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Int.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.tap.common.extensions - -fun Int.isEven() = this and 1 == 0 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 111bad630c..8bd03da756 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -3,7 +3,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer -import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action @@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState { return AppState( globalState = globalReducer(action, state), - homeState = HomeReducer.reduce(action, state), detailsState = DetailsReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), welcomeState = WelcomeReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 3df1fae79b..427a103a98 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState -import com.tangem.tap.features.home.redux.HomeMiddleware -import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware @@ -20,7 +18,6 @@ import org.rekotlin.StateType data class AppState( val globalState: GlobalState = GlobalState(), - val homeState: HomeState = HomeState(), val detailsState: DetailsState = DetailsState(), val walletConnectState: WalletConnectState = WalletConnectState(), val welcomeState: WelcomeState = WelcomeState(), @@ -32,7 +29,6 @@ data class AppState( return listOf( logMiddleware, GlobalMiddleware.handler, - HomeMiddleware.handler, DetailsMiddleware().detailsMiddleware, WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 15530c62b3..f944a89717 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -26,10 +26,9 @@ internal object LegacyMiddleware { { action -> when (action) { is LegacyAction.PrepareDetailsScreen -> { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - userWalletsListManager.selectedUserWallet + selectedUserWallet() .distinctUntilChanged() .onEach { selectedUserWallet -> val initializedAppSettingsStateContent = initializeAppSettingsState( @@ -52,6 +51,16 @@ internal object LegacyMiddleware { } } + private fun selectedUserWallet(): Flow { + val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull() + } else { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + userWalletsListManager.selectedUserWallet + } + } + /** * LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking * previously it was initialized in runBlocking and blocked details screen @@ -64,6 +73,8 @@ internal object LegacyMiddleware { selectedAppCurrency = store.state.globalState.appCurrency, selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, + requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(), + useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(), isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository) .getBalanceHidingSettings().isHidingEnabledInSettings, needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index 6096730a8e..61b24a7d63 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.compose.ui.text.intl.Locale import androidx.core.view.isVisible import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam @@ -14,8 +15,6 @@ import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.inject -import com.tangem.tap.features.home.LocaleRegionProvider -import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store @@ -29,6 +28,7 @@ internal object ScanFailsDialog { private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/" private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/" + private const val RUSSIA_LOCALE = "ru" fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog { return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { @@ -62,8 +62,8 @@ internal object ScanFailsDialog { source = sourceAnalytics, ), ) - val locale = LocaleRegionProvider().getRegion() - val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK + val locale = Locale.current.region + val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK store.dispatchOpenUrl(link) } customView.findViewById(R.id.request_support_button)?.setOnClickListener { diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index dea8dc75a4..d780f3d15a 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -6,7 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented // [REDACTED_JIRA] @@ -28,10 +27,6 @@ internal class RuntimeUserWalletsStore( return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } } - override suspend fun getAllSyncOrNull(): List? { - return userWalletsListManager.userWallets.firstOrNull() - } - override suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt new file mode 100644 index 0000000000..c5275bf034 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.data + +import com.tangem.common.CompletionResult +import com.tangem.common.catching +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class UserWalletsStoreRepositoryProxy( + private val userWalletsListRepository: UserWalletsListRepository, +) : UserWalletsStore { + + override val selectedUserWalletOrNull: UserWallet? + get() = userWalletsListRepository.selectedUserWallet.value + + override val userWallets: Flow> + get() = flow { + userWalletsListRepository.load() + userWalletsListRepository.userWallets.collect { + emit(requireNotNull(it)) + } + } + + override fun getSyncOrNull(key: UserWalletId): UserWallet? { + return userWalletsListRepository.userWallets.value?.find { it.walletId == key } + } + + override fun getSyncStrict(key: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } + } + + override suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult { + return catching { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } + val updatedUserWallet = update(userWallet) + userWalletsListRepository.saveWithoutLock( + userWallet = updatedUserWallet, + canOverride = true, + ) + updatedUserWallet + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt b/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt new file mode 100644 index 0000000000..cd7becb2e0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.di + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.tap.features.intentHandler.IntentProcessor +import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler +import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler +import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object IntentHandlingModule { + + @Provides + @Singleton + fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler() + + @Provides + @Singleton + fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler() + + @Provides + @Singleton + fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler = + OnPushClickedIntentHandler(analyticsEventHandler) + + @Provides + @Singleton + fun provideIntentProcessor(): IntentProcessor = IntentProcessor() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt deleted file mode 100644 index 788367340b..0000000000 --- a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.tap.di.data - -import com.tangem.data.common.network.NetworkFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.sdk.api.TangemSdkManager -import com.tangem.tap.domain.card.DefaultDerivationsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object CardDataModule { - - @Singleton - @Provides - fun providesDerivationsRepository( - tangemSdkManager: TangemSdkManager, - userWalletsStore: UserWalletsStore, - networkFactory: NetworkFactory, - dispatchers: CoroutineDispatcherProvider, - ): DerivationsRepository { - return DefaultDerivationsRepository( - tangemSdkManager = tangemSdkManager, - userWalletsStore = userWalletsStore, - networkFactory = networkFactory, - dispatchers = dispatchers, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt index b2a04f9d52..2fe50e3705 100644 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -2,7 +2,10 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.data.RuntimeUserWalletsStore +import com.tangem.tap.data.UserWalletsStoreRepositoryProxy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +18,15 @@ internal object UserWalletsStoreModule { @Provides @Singleton - fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore { - return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) + fun provideUserWalletsStore( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): UserWalletsStore { + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + UserWalletsStoreRepositoryProxy(userWalletsListRepository) + } else { + RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt new file mode 100644 index 0000000000..8cf8edf8d6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -0,0 +1,52 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.* +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AccountDomainModule { + + @Provides + @Singleton + fun provideAddCryptoPortfolioUseCase(accountsCRUDRepository: AccountsCRUDRepository): AddCryptoPortfolioUseCase { + return AddCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + } + + @Provides + @Singleton + fun provideUpdateCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): UpdateCryptoPortfolioUseCase { + return UpdateCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + } + + @Provides + @Singleton + fun provideArchiveCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): ArchiveCryptoPortfolioUseCase { + return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + } + + @Provides + @Singleton + fun provideRecoverCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): RecoverCryptoPortfolioUseCase { + return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) + } + + @Provides + @Singleton + fun provideGetUnoccupiedAccountIndexUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): GetUnoccupiedAccountIndexUseCase { + return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index fc0942279a..a674e4ac97 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -2,13 +2,18 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.* import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import com.tangem.tap.domain.card.DefaultResetCardUseCase @@ -39,9 +44,16 @@ internal object CardDomainModule { } @Provides - @Singleton - fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) + fun provideIsNeedToBackupUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 7be9d95b9a..5f8a03b7d2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -3,7 +3,9 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import com.tangem.tap.domain.scanCard.LegacyScanProcessor @@ -31,7 +33,15 @@ internal object CardLegacyDomainModule { @Provides @Singleton - fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase { - return GenerateWalletNameUseCase(userWalletsListManager) + fun providesWalletNameGenerateUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GenerateWalletNameUseCase { + return GenerateWalletNameUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 55d82f0af5..89bd3b4ca9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,11 +1,12 @@ package com.tangem.tap.di.domain -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -72,6 +73,7 @@ internal object ManageTokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( customTokensRepository = customTokensRepository, @@ -81,6 +83,7 @@ internal object ManageTokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 1dab6fc6f5..1989f4868e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher @@ -9,9 +9,12 @@ import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -63,6 +66,7 @@ object MarketsDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( derivationsRepository = derivationsRepository, @@ -71,6 +75,7 @@ object MarketsDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } @@ -78,10 +83,14 @@ object MarketsDomainModule { @Singleton fun provideFilterNetworksUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, excludedBlockchains: ExcludedBlockchains, ): FilterAvailableNetworksForWalletUseCase { return FilterAvailableNetworksForWalletUseCase( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, excludedBlockchains = excludedBlockchains, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index c7b61fd1e0..b35ae23d95 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.notifications.* import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.utils.notifications.PushNotificationsTokenProvider @@ -18,20 +19,22 @@ internal object NotificationsDomainModule { @Provides @Singleton - fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase { + fun providesGetApplicationIdUseCase( + pushNotificationsRepository: PushNotificationsRepository, + ): GetApplicationIdUseCase { return GetApplicationIdUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, ) } @Provides @Singleton fun providesSendPushTokenUseCase( - notificationsRepository: NotificationsRepository, + pushNotificationsRepository: PushNotificationsRepository, pushNotificationsTokenProvider: PushNotificationsTokenProvider, ): SendPushTokenUseCase { return SendPushTokenUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -56,6 +59,26 @@ internal object NotificationsDomainModule { ) } + @Provides + @Singleton + fun providesShouldShowNotificationUseCase( + notificationsRepository: NotificationsRepository, + ): ShouldShowNotificationUseCase { + return ShouldShowNotificationUseCase( + notificationsRepository = notificationsRepository, + ) + } + + @Provides + @Singleton + fun providesSetShouldShowNotificationUseCase( + notificationsRepository: NotificationsRepository, + ): SetShouldShowNotificationUseCase { + return SetShouldShowNotificationUseCase( + notificationsRepository = notificationsRepository, + ) + } + @Provides @Singleton fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { @@ -65,8 +88,8 @@ internal object NotificationsDomainModule { @Provides @Singleton fun provideGetNetworksAvailableForNotifications( - notificationsRepository: NotificationsRepository, + pushNotificationsRepository: PushNotificationsRepository, ): GetNetworksAvailableForNotificationsUseCase { - return GetNetworksAvailableForNotificationsUseCase(notificationsRepository = notificationsRepository) + return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index b6152265bf..f98c7d2bd2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -94,12 +94,12 @@ internal object StakingDomainModule { @Provides @Singleton fun provideFetchStakingYieldBalanceUseCase( - stakingErrorResolver: StakingErrorResolver, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): FetchStakingYieldBalanceUseCase { return FetchStakingYieldBalanceUseCase( - stakingErrorResolver = stakingErrorResolver, singleYieldBalanceFetcher = singleYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } @@ -173,18 +173,6 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideIsApproveNeededUseCase( - stakingRepository: StakingRepository, - stakingErrorResolver: StakingErrorResolver, - ): IsApproveNeededUseCase { - return IsApproveNeededUseCase( - stakingRepository = stakingRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - @Provides @Singleton fun provideGetConstructedStakingTransactionUseCase( @@ -209,12 +197,6 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase { - return GetStakingIntegrationIdUseCase(stakingRepository) - } - @Provides @Singleton fun provideCheckAccountInitializedUseCase( @@ -225,9 +207,13 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideGetActionRequirementAmountUseCase( - stakingRepository: StakingRepository, - ): GetActionRequirementAmountUseCase { - return GetActionRequirementAmountUseCase(stakingRepository) + fun provideGetActionRequirementAmountUseCase(): GetActionRequirementAmountUseCase { + return GetActionRequirementAmountUseCase() + } + + @Provides + @Singleton + fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory { + return StakingIdFactory(walletManagersFacade = walletManagersFacade) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 819dab82ba..4a0a3c5922 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -11,12 +11,13 @@ import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -46,6 +47,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, tokensFeatureToggles: TokensFeatureToggles, + stakingIdFactory: StakingIdFactory, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, @@ -54,6 +56,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -64,12 +67,14 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): FetchTokenListUseCase { return FetchTokenListUseCase( currenciesRepository = currenciesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } @@ -91,11 +96,11 @@ internal object TokensDomainModule { @Singleton fun provideGetTokenListUseCase( currenciesRepository: CurrenciesRepository, - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetTokenListUseCase { return GetTokenListUseCase( currenciesRepository = currenciesRepository, - currenciesStatusesOperations = baseCurrenciesStatusesOperations, + currenciesStatusesOperations = currenciesStatusesOperations, ) } @@ -172,6 +177,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, tokensFeatureToggles: TokensFeatureToggles, + stakingIdFactory: StakingIdFactory, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( currenciesRepository = currenciesRepository, @@ -180,6 +186,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -190,12 +197,14 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( currenciesRepository = currenciesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } @@ -359,9 +368,9 @@ internal object TokensDomainModule { @Provides @Singleton fun provideGetWalletTotalBalanceUseCase( - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations) + return GetWalletTotalBalanceUseCase(currenciesStatusesOperations) } @Provides @@ -389,47 +398,12 @@ internal object TokensDomainModule { return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers) } - @Provides - @Singleton - fun provideBaseCurrenciesStatusesOperations( - tokensFeatureToggles: TokensFeatureToggles, - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - stakingRepository: StakingRepository, - singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - ): BaseCurrenciesStatusesOperations { - return CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - stakingRepository = stakingRepository, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, - tokensFeatureToggles = tokensFeatureToggles, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - ) - } - @Provides @Singleton fun provideBaseCurrencyStatusOperations( tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -437,13 +411,14 @@ internal object TokensDomainModule { multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceSupplier: MultiYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + stakingIdFactory: StakingIdFactory, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, - stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -451,9 +426,11 @@ internal object TokensDomainModule { multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -472,6 +449,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { return WalletBalanceFetcher( @@ -481,6 +459,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index d9a7d2b4ad..d7c8721bff 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.data.wallets.hot.TangemHotWalletSigner import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher @@ -11,7 +12,6 @@ import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.tap.domain.hot.TangemHotWalletSigner import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -181,8 +181,13 @@ internal object TransactionDomainModule { fun providePrepareForSendUseCase( transactionRepository: TransactionRepository, cardSdkConfigRepository: CardSdkConfigRepository, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, ): PrepareForSendUseCase { - return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository) + return PrepareForSendUseCase( + transactionRepository = transactionRepository, + cardSdkConfigRepository = cardSdkConfigRepository, + getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) }, + ) } @Provides @@ -199,8 +204,13 @@ internal object TransactionDomainModule { fun provideSignUseCase( walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, ): SignUseCase { - return SignUseCase(cardSdkConfigRepository, walletManagersFacade) + return SignUseCase( + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) }, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 87b107936a..44dd702fc3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -10,11 +10,13 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -31,18 +33,30 @@ internal object WalletsDomainModule { @Provides fun providesUserWalletsSyncDelegate( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): UserWalletsSyncDelegate { return DefaultUserWalletsSyncDelegate( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, dispatchers = dispatchers, ) } @Provides @Singleton - fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { - return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetWalletsUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetWalletsUseCase { + return GetWalletsUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -50,37 +64,79 @@ internal object WalletsDomainModule { fun providesWalletNameMigrationUseCase( userWalletsListManager: UserWalletsListManager, walletNamesMigrationRepository: WalletNamesMigrationRepository, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): WalletNameMigrationUseCase { return WalletNameMigrationUseCase( userWalletsListManager = userWalletsListManager, walletNamesMigrationRepository = walletNamesMigrationRepository, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } @Provides @Singleton - fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { - return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetUserWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetUserWalletUseCase { + return GetUserWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton fun providesGetSelectedWalletSyncUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) + return GetSelectedWalletSyncUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetSelectedWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { - return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesSaveWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + walletsRepository: WalletsRepository, + ): SaveWalletUseCase { + return SaveWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + walletsRepository = walletsRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) + } + + @Provides + @Singleton + fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase { + return GenerateBuyTangemCardLinkUseCase() } @Provides @@ -99,15 +155,30 @@ internal object WalletsDomainModule { @Singleton fun providesSelectWalletUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, reduxStateHolder: ReduxStateHolder, ): SelectWalletUseCase { - return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder) + return SelectWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + reduxStateHolder = reduxStateHolder, + ) } @Provides @Singleton - fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { - return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesUpdateWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): UpdateWalletUseCase { + return UpdateWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -124,14 +195,30 @@ internal object WalletsDomainModule { @Provides @Singleton - fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase { - return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetWalletsSyncUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetWalletNamesUseCase { + return GetWalletNamesUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { - return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesDeleteWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): DeleteWalletUseCase { + return DeleteWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -214,9 +301,13 @@ internal object WalletsDomainModule { @Singleton fun providesGetSavedWalletChangesIdUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSavedWalletsCountUseCase { return GetSavedWalletsCountUseCase( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } diff --git a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt index f68e642f94..c21b55928b 100644 --- a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt +++ b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt @@ -1,8 +1,6 @@ package com.tangem.tap.di.hot import com.tangem.hot.sdk.TangemHotSdk -import com.tangem.tap.domain.hot.HotWalletPasswordRequester -import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester import com.tangem.tap.features.hot.TangemHotSDKProxy import dagger.Binds import dagger.Module @@ -17,8 +15,4 @@ internal interface TangemHotSdkModule { @Binds @Singleton fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk - - @Binds - @Singleton - fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt deleted file mode 100644 index a65a2ecd53..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.tap.domain.hot - -import com.tangem.common.core.TangemSdkError -import com.tangem.features.hotwallet.HotWalletPasswordRequester -import com.tangem.hot.sdk.TangemHotSdk -import com.tangem.hot.sdk.exception.WrongPasswordException -import com.tangem.hot.sdk.model.* -import javax.inject.Inject - -class HotWalletAccessor @Inject constructor( - private val tangemHotSdk: TangemHotSdk, - private val hotWalletPasswordRequester: HotWalletPasswordRequester, -) { - - suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List { - val auth = when (hotWalletId.authType) { - HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth - HotWalletId.AuthType.Password -> requestPassword(false) - HotWalletId.AuthType.Biometry -> HotAuth.Biometry - } - - return runCatchingSdkErrors(hotWalletId, auth) { - tangemHotSdk.signHashes( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = it, - ), - dataToSign = dataToSign, - ).also { - hotWalletPasswordRequester.dismiss() - } - } - } - - private suspend fun runCatchingSdkErrors( - hotWalletId: HotWalletId, - auth: HotAuth, - block: suspend (auth: HotAuth) -> T, - ): T { - return runCatchingWrongPassInternal( - originalAuth = auth, - auth = auth, - block = { blockAuth -> - block(blockAuth).also { - // TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method - if (blockAuth is HotAuth.Password /*&& has biometry enabled */) { - tangemHotSdk.changeAuth( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = blockAuth, - ), - auth = HotAuth.Biometry, - ) - } - } - }, - ) - } - - private suspend fun runCatchingWrongPassInternal( - originalAuth: HotAuth, - auth: HotAuth, - block: suspend (auth: HotAuth) -> T, - ): T = runCatching { - block(auth) - }.getOrElse { exception -> - if (auth is HotAuth.Biometry && exception.isBiometryError()) { - // fallback to password if biometry fails - val passAuth = requestPassword(true) - - return@getOrElse runCatchingWrongPassInternal( - originalAuth = originalAuth, - auth = passAuth, - block = block, - ) - } - - if (exception !is WrongPasswordException) { - throw exception - } - - // If the exception is a wrong password, we need to request the password again - - hotWalletPasswordRequester.wrongPassword() - val passResult = requestPassword(originalAuth is HotAuth.Biometry) - - runCatchingWrongPassInternal( - originalAuth = originalAuth, - auth = passResult, - block = block, - ) - } - - private suspend fun requestPassword(hasBiometry: Boolean): HotAuth { - return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled() - } - - private fun Throwable.isBiometryError(): Boolean { - return this is TangemSdkError.AuthenticationFailed || - this is TangemSdkError.AuthenticationCanceled || - this is TangemSdkError.AuthenticationLockout || - this is TangemSdkError.AuthenticationUnavailable || - this is TangemSdkError.AuthenticationAlreadyInProgress || - this is TangemSdkError.AuthenticationNotInitialized || - this is TangemSdkError.AuthenticationPermanentLockout - } - - private fun HotWalletPasswordRequester.Result.toAuth() = when (this) { - HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry - HotWalletPasswordRequester.Result.Dismiss -> null - is HotWalletPasswordRequester.Result.EnteredPassword -> this.password - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt deleted file mode 100644 index 1f80fd785f..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.domain.hot - -import com.tangem.hot.sdk.model.HotAuth -import com.tangem.hot.sdk.model.HotWalletId - -interface HotWalletPasswordRequester { - - suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt b/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt deleted file mode 100644 index 0b71ca974c..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.tangem.tap.domain.hot - -import com.tangem.blockchain.common.TransactionSigner -import com.tangem.blockchain.common.Wallet -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError -import com.tangem.common.map -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.hot.sdk.model.DataToSign -import com.tangem.operations.sign.SignData -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -class TangemHotSigner @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Hot, - private val hotWalletAccessor: HotWalletAccessor, -) : TransactionSigner { - - override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult { - return sign(listOf(hash), publicKey).map { it.first() } - } - - override suspend fun sign( - hashes: List, - publicKey: Wallet.PublicKey, - ): CompletionResult> { - val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey } - ?: return CompletionResult.Failure( - TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), - ) - - val result = hotWalletAccessor.signHashes( - hotWalletId = userWallet.hotWalletId, - dataToSign = listOf( - DataToSign( - curve = wallet.curve, - hashes = hashes, - derivationPath = publicKey.derivationPath, - ), - ), - ) - - return CompletionResult.Success(result.map { it.signatures }.flatten()) - } - - override suspend fun multiSign( - dataToSign: List, - publicKey: Wallet.PublicKey, - ): CompletionResult> { - val result = hotWalletAccessor.signHashes( - hotWalletId = userWallet.hotWalletId, - dataToSign = dataToSign.map { signData -> - val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey } - ?: return CompletionResult.Failure( - TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), - ) - - DataToSign( - curve = wallet.curve, - hashes = listOf(signData.hash), - derivationPath = signData.derivationPath, - ) - }, - ) - - return CompletionResult.Success( - result.mapIndexed { index, data -> - dataToSign[index].publicKey to data.signatures.first() - }.toMap(), - ) - } - - @AssistedFactory - interface Factory { - fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 83c7a9c1fb..09a9039974 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -20,13 +20,11 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.* import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse @@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager( ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = VisaCustomerWalletApproveTask( - visaDataForApprove = visaDataForApprove, + VisaCustomerWalletApproveTask.Input( + cardId = visaDataForApprove.customerWalletCardId, + targetAddress = visaDataForApprove.targetAddress, + hashToSign = visaDataForApprove.dataToSign.hashToSign, + sign = visaDataForApprove.dataToSign::sign, + ), ), cardId = visaDataForApprove.customerWalletCardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index edca4bea1a..1568e7dfc8 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index acd5a2e14a..fba95394c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -14,7 +14,7 @@ import com.tangem.common.map import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.CardTypesResolver -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index 6e6a912b0d..fa255fdacb 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.wallet.UserWalletId diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 90275890f2..df05ce2f7e 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -14,14 +14,14 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.common.TwinsHelper -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 3ef65f5c16..4fa773c18c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card @@ -10,27 +11,24 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError -import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet -import com.tangem.domain.visa.model.sign import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand class VisaCustomerWalletApproveTask( - private val visaDataForApprove: VisaDataForApprove, + private val visaDataForApprove: Input, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { @@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask( return } - if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { + if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) { callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) return } @@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask( ) } + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK + private fun hashPersonalMessage(message: ByteArray): ByteArray { + val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() + return (prefix + message).toKeccak() + } + private fun signApproveData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, @@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask( session: CardSession, callback: CompletionCallback, ) { - val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes() + val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}" + val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) val signTask = SignHashCommand( - hash = hashToSign, + hash = hash, walletPublicKey = targetWalletPublicKey, derivationPath = derivationPath, ) @@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask( is CompletionResult.Success -> { val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( signature = result.data.signature, - hash = hashToSign, + hash = hash, publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() ?: targetWalletPublicKey.toDecompressedPublicKey(), ).asRSVLegacyEVM().toHexString().lowercase() @@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask( scanCard( session = session, callback = callback, - signedData = visaDataForApprove.dataToSign.sign( - signature = rsvSignature, - customerWalletAddress = visaDataForApprove.targetAddress, - ), + signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress), ) } is CompletionResult.Failure -> { @@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask( } } } + + data class Input( + val cardId: String? = null, + val targetAddress: String, + val hashToSign: String, + val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 86be1e5223..c521709443 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -11,14 +11,19 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager +import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository @@ -26,6 +31,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository import com.tangem.tap.tangemSdkManager import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -40,6 +46,7 @@ internal object UserWalletsListManagerModule { @Provides @Singleton + @Deprecated("Use UserWalletsListRepository instead") fun provideGeneralUserWalletsListManager( @ApplicationContext applicationContext: Context, appPreferencesStore: AppPreferencesStore, @@ -58,42 +65,14 @@ internal object UserWalletsListManagerModule { ) } + @Deprecated("Use UserWalletsListRepository instead") private fun createBiometricUserWalletsListManager( applicationContext: Context, analyticsEventHandler: AnalyticsEventHandler, dispatchers: CoroutineDispatcherProvider, ): UserWalletsListManager { - val moshi = Moshi.Builder() - .add(WalletDerivedKeysMapAdapter()) - .add(ScanResponseDerivedKeysMapAdapter()) - .add(ByteArrayKeyAdapter()) - .add(ExtendedPublicKeysMapAdapter()) - .add(CardBackupStatusAdapter()) - .add(DerivationPathAdapterWithMigration()) - .add(TangemSdkAdapter.DateAdapter()) - .add(TangemSdkAdapter.DerivationNodeAdapter()) - .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model - .add(VisaActivationRemoteState.jsonAdapter) - .add(VisaCardActivationStatus.jsonAdapter) - .addLast(KotlinJsonAdapterFactory()) - .build() - - val secureStorage = AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "user_wallets_storage", - ), - androidSecureStorageV2 = AndroidSecureStorageV2( - appContext = applicationContext, - useStrongBox = true, - name = "user_wallets_storage2", - ), - androidSecureStorageV3 = AndroidSecureStorageV2( - appContext = applicationContext, - useStrongBox = false, - name = "user_wallets_storage3", - ), - ) + val moshi = buildMoshi() + val secureStorage = buildSecureStorage(applicationContext = applicationContext) val authenticatedStorage = AuthenticatedStorage( secureStorage = UserWalletsKeysStoreDecorator( @@ -134,4 +113,97 @@ internal object UserWalletsListManagerModule { selectedUserWalletRepository = selectedUserWalletRepository, ) } + + @Provides + @Singleton + fun provideUserWalletsListRepository( + @ApplicationContext applicationContext: Context, + dispatchers: CoroutineDispatcherProvider, + passwordRequester: HotWalletPasswordRequester, + appPreferencesStore: AppPreferencesStore, + hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + ): UserWalletsListRepository { + val moshi = buildMoshi() + val secureStorage = buildSecureStorage(applicationContext = applicationContext) + + val authenticatedStorage = AuthenticatedStorage( + secureStorage = UserWalletsKeysStoreDecorator( + featureStorage = secureStorage, + cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, + ), + keystoreManager = DelegatedKeystoreManager( + keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, + ), + ) + + val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( + moshi = moshi, + secureStorage = secureStorage, + ) + + val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository( + moshi = moshi, + secureStorage = secureStorage, + ) + + val selectedUserWalletRepository = DefaultSelectedUserWalletRepository( + secureStorage = secureStorage, + dispatchers = dispatchers, + ) + + val userWalletEncryptionKeysRepository = UserWalletEncryptionKeysRepository( + moshi = moshi, + authenticatedStorage = authenticatedStorage, + dispatchers = dispatchers, + secureStorage = secureStorage, + ) + + return DefaultUserWalletsListRepository( + publicInformationRepository = publicInformationRepository, + sensitiveInformationRepository = sensitiveInformationRepository, + selectedUserWalletRepository = selectedUserWalletRepository, + passwordRequester = passwordRequester, + userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository, + tangemSdkManagerProvider = Provider { tangemSdkManager }, + appPreferencesStore = appPreferencesStore, + savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now + hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository, + ) + } + + fun buildMoshi(): Moshi { + return Moshi.Builder() + .add(WalletDerivedKeysMapAdapter()) + .add(ScanResponseDerivedKeysMapAdapter()) + .add(ByteArrayKeyAdapter()) + .add(ExtendedPublicKeysMapAdapter()) + .add(CardBackupStatusAdapter()) + .add(DerivationPathAdapterWithMigration()) + .add(TangemSdkAdapter.DateAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaActivationRemoteState.jsonAdapter) + .add(VisaCardActivationStatus.jsonAdapter) + .addLast(KotlinJsonAdapterFactory()) + .build() + } + + fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage { + return AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "user_wallets_storage", + ), + androidSecureStorageV2 = AndroidSecureStorageV2( + appContext = applicationContext, + useStrongBox = true, + name = "user_wallets_storage2", + ), + androidSecureStorageV3 = AndroidSecureStorageV2( + appContext = applicationContext, + useStrongBox = false, + name = "user_wallets_storage3", + ), + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt new file mode 100644 index 0000000000..93810c48da --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -0,0 +1,420 @@ +package com.tangem.tap.domain.userWalletList.repository + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.flatMap +import com.tangem.common.map +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.wallets.R +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.domain.core.wallets.error.DeleteWalletError +import com.tangem.domain.core.wallets.error.LockWalletsError +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.core.wallets.error.SelectWalletError +import com.tangem.domain.core.wallets.error.SetLockError +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey +import com.tangem.tap.domain.userWalletList.utils.encryptionKey +import com.tangem.tap.domain.userWalletList.utils.lock +import com.tangem.tap.domain.userWalletList.utils.toUserWallets +import com.tangem.tap.domain.userWalletList.utils.updateWith +import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend +import com.tangem.utils.extensions.indexOfFirstOrNull +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Suppress("LongParameterList", "LargeClass") +internal class DefaultUserWalletsListRepository( + private val publicInformationRepository: UserWalletsPublicInformationRepository, + private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository, + private val selectedUserWalletRepository: SelectedUserWalletRepository, + private val passwordRequester: HotWalletPasswordRequester, + private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository, + private val tangemSdkManagerProvider: Provider, + private val savePersistentInformation: ProviderSuspend, + private val appPreferencesStore: AppPreferencesStore, + private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, +) : UserWalletsListRepository { + + override val userWallets = MutableStateFlow?>(null) + override val selectedUserWallet = MutableStateFlow(null) + + override suspend fun load() { + if (userWallets.value != null) return + + if (savePersistentInformation().not()) { + // If we don't save persistent information, we don't need to load user wallets + // and we should clear any existing data + clearPersistentData() + userWallets.value = emptyList() + return + } + + val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured() + + publicInformationRepository.getAll() + .map { it.toUserWallets() } + .flatMap { wallets -> + sensitiveInformationRepository.getAll(unsecuredEncryptionKeys) + .map { wallets.updateWith(it) } + }.doOnSuccess { + userWallets.value = it + } + + val selectedUserWalletId = selectedUserWalletRepository.get() + selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId } + ?: userWallets.value?.firstOrNull() + } + + override suspend fun userWalletsSync(): List { + load() + return requireNotNull(userWallets.value) { + "This should never happen" + } + } + + override suspend fun selectedUserWalletSync(): UserWallet? { + load() + return selectedUserWallet.value + } + + override suspend fun select(userWalletId: UserWalletId): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(SelectWalletError.UnableToSelectUserWallet) + selectedUserWalletRepository.set(userWalletId) + selectedUserWallet.value = userWallet + userWallet + } + + override suspend fun saveWithoutLock( + userWallet: UserWallet, + canOverride: Boolean, + ): Either = either { + if (canOverride.not() && userWallets.value?.any { it.walletId == userWallet.walletId } == true) { + raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved)) + } + + if (savePersistentInformation()) { + publicInformationRepository.save(userWallet, canOverride) + if (userWallet.isLocked.not()) { + sensitiveInformationRepository.save(userWallet, userWallet.encryptionKey) + } + } + + // update the userWallets state and add if it doesn't exist + userWallets.update { currentWallets -> + val wallets = currentWallets ?: emptyList() + if (wallets.any { it.walletId == userWallet.walletId }) { + wallets.map { if (it.walletId == userWallet.walletId) userWallet else it } + } else { + wallets + userWallet + } + } + + // update the selectedUserWallet state if it is the only wallet + if (userWallets.value?.size == 1) { + selectedUserWalletRepository.set(userWallet.walletId) + selectedUserWallet.value = userWallet + } + + userWallet + } + + override suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean, + ): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(SetLockError.UserWalletNotFound) + + val encryptionKey = userWallet.encryptionKey + ?: raise(SetLockError.UserWalletLocked) + + runCatching { + userWalletEncryptionKeysRepository.save( + encryptionKey = UserWalletEncryptionKey( + walletId = userWalletId, + encryptionKey = encryptionKey, + ), + removeUnsecured = changeUnsecured, + method = when (lockMethod) { + is LockMethod.AccessCode -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode) + } + LockMethod.Biometric -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric + } + LockMethod.NoLock -> { + if (userWallet is UserWallet.Cold) { + raise(SetLockError.UserWalletNotFound) + } + + UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured + } + }, + ) + }.onFailure { raise(SetLockError.UnableToSetLock(it)) } + } + + override suspend fun removeBiometricLock(userWalletId: UserWalletId) { + userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId) + } + + override suspend fun delete(userWalletIds: List): Either = either { + if (userWalletIds.isEmpty()) return Unit.right() + + publicInformationRepository.delete(userWalletIds) + .doOnFailure { + raise(DeleteWalletError.UnableToDelete) + } + sensitiveInformationRepository.delete(userWalletIds) + .doOnFailure { + raise(DeleteWalletError.UnableToDelete) + } + + userWalletEncryptionKeysRepository.delete(userWalletIds) + + val userWalletsBeforeDelete = userWallets.value ?: return@either + + userWallets.update { currentWallets -> + currentWallets?.filterNot { it.walletId in userWalletIds } + } + + selectedUserWallet.update { currentSelected -> + if (currentSelected == null) return@update null + + userWallets.value?.findAvailableUserWallet( + userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0, + ) + } + } + + override suspend fun unlock( + userWalletId: UserWalletId, + unlockMethod: UserWalletsListRepository.UnlockMethod, + ): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(UnlockWalletError.UserWalletNotFound) + + if (userWallet.isLocked.not()) { + raise(UnlockWalletError.AlreadyUnlocked) + } + + when (unlockMethod) { + UserWalletsListRepository.UnlockMethod.Biometric -> { + unlockAllWallets().bind() + select(userWalletId) + } + UserWalletsListRepository.UnlockMethod.AccessCode -> { + if (userWallet !is UserWallet.Hot) { + raise(UnlockWalletError.UnableToUnlock) + } + + val encryptionKey = requestPasswordRecursive( + hotWalletId = userWallet.hotWalletId, + block = { password -> + runCatching { + userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) + }.onFailure { + raise(UnlockWalletError.UnableToUnlock) + }.getOrNull() + }, + biometryFallback = { + unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric) + }, + ).bind() + + if (encryptionKey == null) { + return@either + } + + removePasswordAttempts(userWallet) + + sensitiveInformationRepository.getAll(listOf(encryptionKey)) + .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } + .doOnFailure { error -> + raise(UnlockWalletError.UnableToUnlock) + } + } + UserWalletsListRepository.UnlockMethod.Scan -> { + if (userWallet !is UserWallet.Cold) { + raise(UnlockWalletError.UnableToUnlock) + } + + tangemSdkManagerProvider().scanProduct() + .doOnSuccess { scanResponse -> + val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build() + + if (expectedId != userWallet.walletId) { + raise(UnlockWalletError.ScannedCardWalletNotMatched) + } + + saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true) + .mapLeft { UnlockWalletError.UnableToUnlock } + .bind() + } + .doOnFailure { + raise(UnlockWalletError.UserCancelled) + } + } + } + } + + override suspend fun unlockAllWallets(): Either = either { + val userWalletIds = userWalletsSync().map { it.walletId }.toSet() + val biometricKeys = runCatching { + userWalletEncryptionKeysRepository.getAllBiometric() + }.getOrElse { + // TODO handle error properly [REDACTED_TASK_KEY] + raise(UnlockWalletError.UserCancelled) + } + + val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() + val allKeys = (biometricKeys + unsecuredKeys).distinct() + val unlockedWalletsIds = allKeys.map { it.walletId } + + val unlockedWallets = unlockedWalletsIds.mapNotNull { id -> + userWalletsSync().firstOrNull { it.walletId == id } + } + + // Remove all password attempts for unlocked hot wallets + unlockedWallets.forEach { + removePasswordAttempts(it) + } + + // if we cant unlock all wallets + if (userWalletIds.all { it in unlockedWalletsIds }.not()) { + raise(UnlockWalletError.UnableToUnlock) + } + + sensitiveInformationRepository.getAll(allKeys) + .doOnSuccess { sensitiveInfo -> + userWallets.update { it?.updateWith(sensitiveInfo) } + } + .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } + } + + override suspend fun lockAllWallets(): Either = either { + val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet() + + if (unsecuredWalletIds.size == userWallets.value?.size) { + raise(LockWalletsError.NothingToLock) + } + + userWallets.update { + it?.map { + if (it.walletId !in unsecuredWalletIds) { + it.lock() + } else { + it + } + } + } + } + + override suspend fun clearPersistentData() { + publicInformationRepository.clear() + sensitiveInformationRepository.clear() + userWalletEncryptionKeysRepository.clear() + } + + private suspend fun requestPasswordRecursive( + hotWalletId: HotWalletId, + block: suspend (CharArray) -> UserWalletEncryptionKey?, + biometryFallback: suspend () -> Either, + ): Either { + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts + hasBiometry = hasBiometry(), + ) + val result = passwordRequester.requestPassword(attemptRequest) + + return when (result) { + HotWalletPasswordRequester.Result.Dismiss -> { + passwordRequester.dismiss() + UnlockWalletError.UserCancelled.left() + } + is HotWalletPasswordRequester.Result.EnteredPassword -> { + val decrypted = block(result.password.value) + if (decrypted == null) { + passwordRequester.wrongPassword() + requestPasswordRecursive(hotWalletId, block, biometryFallback) + } else { + passwordRequester.successfulAuthentication() + passwordRequester.dismiss() + decrypted.right() + } + } + HotWalletPasswordRequester.Result.UseBiometry -> { + biometryFallback() + .onRight { + passwordRequester.successfulAuthentication() + passwordRequester.dismiss() + } + .map { null } + } + } + } + + private suspend fun removePasswordAttempts(userWallet: UserWallet) { + if (userWallet is UserWallet.Hot) { + hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId) + } + } + + private suspend fun hasBiometry(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + default = false, + ) + + return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication + } + + /** + * Find the nearest available wallet that can be selected + * + * Example: + * Number with *n* is previous selected wallet with index [prevSelectedIndex]. + * + * 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4] + * 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4] + * 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*] + * 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*] + * + * @receiver list of user wallets without deleted wallet + */ + private fun List.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? { + if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull() + + if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex] + + for (offset in 1..size) { + val rightIndex = prevSelectedIndex + offset + if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex] + + val leftIndex = prevSelectedIndex - offset + if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex] + } + + return lastOrNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt new file mode 100644 index 0000000000..104cfc0945 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -0,0 +1,196 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.common.authentication.storage.AuthenticatedStorage +import com.tangem.common.services.secure.SecureStorage +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol +import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class UserWalletEncryptionKeysRepository( + moshi: Moshi, + private val authenticatedStorage: AuthenticatedStorage, + private val dispatchers: CoroutineDispatcherProvider, + private val secureStorage: SecureStorage, +) { + + private val encryptionKeyAdapter: JsonAdapter = moshi.adapter( + UserWalletEncryptionKey::class.java, + ) + private val userWalletsIdsListAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, UserWalletId::class.java), + ) + + suspend fun save( + encryptionKey: UserWalletEncryptionKey, + removeUnsecured: Boolean = true, + method: EncryptionMethod, + ) = withContext(dispatchers.io) { + if (removeUnsecured) { + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name) + } + + when (method) { + EncryptionMethod.Unsecured -> { + secureStorage.store( + account = StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } + is EncryptionMethod.Password -> { + val encodedWithPass = AESEncryptionProtocol.encryptWithPassword( + password = method.password, + content = encryptionKey.encode(), + ) + secureStorage.store( + account = StorageKey.UserWalletEncryptionKeyEncrypted(encryptionKey.walletId).name, + data = encodedWithPass, + ) + } + EncryptionMethod.Biometric -> { + authenticatedStorage.store( + keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } + } + + storeUserWalletId(userWalletId = encryptionKey.walletId) + } + + fun removeBiometricKey(userWalletId: UserWalletId) { + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + + suspend fun getAllUnsecured(): List = withContext(dispatchers.io) { + getUserWalletsIds().mapNotNull { userWalletId -> + secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey() + } + } + + suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? = + withContext(dispatchers.io) { + val encrypted = secureStorage.get( + account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name, + ) ?: return@withContext null + + withContext(dispatchers.default) { + AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey() + } + } + + suspend fun getAllBiometric(): List = withContext(dispatchers.io) { + val keys = getUserWalletsIds().map { userWalletId -> + StorageKey.UserWalletEncryptionKey(userWalletId).name + } + + authenticatedStorage.get(keys).mapNotNull { + it.value.decodeToKey() + } + } + + suspend fun delete(userWalletIds: List) { + if (userWalletIds.isEmpty()) return + + withContext(dispatchers.io) { + userWalletIds.forEach { userWalletId -> + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name) + secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name) + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + + val userWalletsIds = getUserWalletsIds().filterNot { it in userWalletIds } + secureStorage.store(userWalletsIds.encode(), StorageKey.UserWalletIds.name) + } + } + + suspend fun clear() { + withContext(dispatchers.io) { + val userWalletsIds = getUserWalletsIds() + userWalletsIds.forEach { userWalletId -> + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name) + secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name) + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + secureStorage.delete(StorageKey.UserWalletIds.name) + } + } + + private suspend fun getUserWalletsIds(): List { + return withContext(dispatchers.io) { + secureStorage.get(StorageKey.UserWalletIds.name) + .decodeToUserWalletsIds() + } + } + + private suspend fun storeUserWalletId(userWalletId: UserWalletId) { + val userWalletIds = (getUserWalletsIds() + userWalletId).distinct() + + withContext(dispatchers.io) { + secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name) + } + } + + private suspend fun UserWalletEncryptionKey.encode(): ByteArray { + return withContext(dispatchers.default) { + this@encode + .let(encryptionKeyAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? { + return withContext(dispatchers.default) { + this@decodeToKey + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(encryptionKeyAdapter::fromJson) + } + } + + private suspend fun List.encode(): ByteArray { + return withContext(dispatchers.default) { + this@encode + .let(userWalletsIdsListAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray?.decodeToUserWalletsIds(): List { + return withContext(dispatchers.default) { + this@decodeToUserWalletsIds + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(userWalletsIdsListAdapter::fromJson) + .orEmpty() + } + } + + sealed class EncryptionMethod { + data object Unsecured : EncryptionMethod() + data object Biometric : EncryptionMethod() + class Password(val password: CharArray) : EncryptionMethod() + } + + private sealed interface StorageKey { + val name: String + + class UserWalletEncryptionKeyUnsecured(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_unsecured_${userWalletId.stringValue}" + } + + class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}" + } + + class UserWalletEncryptionKeyEncrypted(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_encrypted_${userWalletId.stringValue}" + } + + object UserWalletIds : StorageKey { + override val name: String = "user_wallets_ids_with_saved_keys" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 6992564847..6c9e9f189d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule { wcSessionsRepository: WalletConnectSessionsRepository, currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, walletConnectFeatureToggles: WalletConnectFeatureToggles, coroutineDispatcherProvider: CoroutineDispatcherProvider, + getSelectedWalletUseCase: GetSelectedWalletUseCase, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule { blockchainHelper = TangemWcBlockchainHelper(), currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, - userWalletsListManager = userWalletsListManager, + getSelectedWalletUseCase = getSelectedWalletUseCase, dispatchers = coroutineDispatcherProvider, walletConnectFeatureToggles = walletConnectFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index f602ed8fc4..395ad23885 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.model.legacy.Session import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.extensions.dispatchOnMain @@ -38,18 +37,14 @@ class WalletConnectInteractor( private val dispatchers: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val userWalletsListManager: UserWalletsListManager, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, val blockchainHelper: WcBlockchainHelper, ) { private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled } private var isWalletConnectReadyForDeepLinks = false - private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { - GetSelectedWalletUseCase(userWalletsListManager) - } - private val wcScope = CoroutineScope( SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index af4c05b261..9281cbc507 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -64,6 +66,14 @@ class DetailsMiddleware { when (action.setting) { AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) + AppSetting.RequireAccessCode -> toggleRequireAccessCode( + state = state, + enable = action.enable, + ) + AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication( + state = state, + enable = action.enable, + ) } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { @@ -90,6 +100,91 @@ class DetailsMiddleware { } } + private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) { + scope.launch { + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + + // Nothing to change + if (walletsRepository.useBiometricAuthentication() == enable) { + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + return@launch + } + + toggleRequireAccessCode( + state = state, + enable = true, + ) + + if (enable) { + setBiometricLockForAllWallets() + } else { + // Remove all biometric-related data + removeAllBiometricData() + } + + walletsRepository.setUseBiometricAuthentication(value = enable) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + } + } + + private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) { + scope.launch { + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + + // Nothing to change + if (walletsRepository.requireAccessCode() == enable) { + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + return@launch + } + + if (enable) { + // Remove all biometric sign data + removeAllBiometricSingData() + toggleSaveAccessCodes(state, enable = false) + } else { + toggleSaveAccessCodes(state, enable = true) + } + + walletsRepository.setRequireAccessCode(value = enable) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + } + } + + private suspend fun setBiometricLockForAllWallets() { + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val userWallets = userWalletsListRepository.userWalletsSync() + userWallets.forEach { + userWalletsListRepository.setLock( + userWalletId = it.walletId, + lockMethod = LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + + private suspend fun removeAllBiometricData() { + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + userWalletsListRepository.userWalletsSync().forEach { + userWalletsListRepository.removeBiometricLock(it.walletId) + } + removeAllBiometricSingData() + } + + private suspend fun removeAllBiometricSingData() { + deleteSavedAccessCodes() + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk) + userWalletsListRepository.userWalletsSync().forEach { + if (it is UserWallet.Hot) { + userWalletsListRepository.saveWithoutLock( + userWallet = it.copy( + hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId), + ), + ) + } + } + } + private fun observeBiometricsStatusChanges(scope: CoroutineScope) { val needEnrollBiometricsFlow = flow { do { @@ -233,7 +328,7 @@ class DetailsMiddleware { deleteSavedAccessCodes() store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - store.dispatchNavigationAction { replaceAll(AppRoute.Home) } + store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } return CompletionResult.Success(Unit) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a6f2b28868..e783c18e37 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta ) } +@Suppress("LongMethod", "CyclomaticComplexMethod") private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { return when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( @@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail saveWallets = true, // User can't enable access codes saving without wallets saving saveAccessCodes = action.enable, ) + AppSetting.RequireAccessCode -> state.appSettingsState.copy( + isInProgress = true, + requireAccessCode = action.enable, + ) + AppSetting.BiometricAuthentication -> state.appSettingsState.copy( + isInProgress = true, + useBiometricAuthentication = action.enable, + ) }, ) is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy( @@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail isInProgress = false, saveAccessCodes = action.prevState, ) + AppSetting.RequireAccessCode -> state.appSettingsState.copy( + isInProgress = false, + requireAccessCode = action.prevState, + ) + AppSetting.BiometricAuthentication -> state.appSettingsState.copy( + isInProgress = false, + needEnrollBiometrics = action.prevState, + ) }, ) is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index bc7f17a7b6..980cacb286 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -12,9 +12,14 @@ data class DetailsState( ) : StateType data class AppSettingsState( + @Deprecated("Delete after hot wallet release") val saveWallets: Boolean = false, + @Deprecated("Delete after hot wallet release") val saveAccessCodes: Boolean = false, + @Deprecated("Delete after hot wallet release") val isBiometricsAvailable: Boolean = false, + val requireAccessCode: Boolean = false, + val useBiometricAuthentication: Boolean = false, val needEnrollBiometrics: Boolean = false, val isHidingEnabled: Boolean = false, val isInProgress: Boolean = false, @@ -25,5 +30,5 @@ data class AppSettingsState( enum class SecurityOption { LongTap, PassCode, AccessCode } enum class AppSetting { - SaveWallets, SaveAccessCode + SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 43e66b065a..73459bc7b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory { onDismiss = onDismiss, ) } + + fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference( + R.string.app_settings_off_biometrics_alert_message, + wrappedList(resourceReference(R.string.common_biometrics)), + ), + confirmText = resourceReference(R.string.common_disable), + onConfirm = onDisable, + onDismiss = onDismiss, + ) + } + + fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_on_require_access_code_alert_message), + confirmText = resourceReference(R.string.common_enable), + onConfirm = { onEnable() }, + onDismiss = onDismiss, + ) + } + + fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_require_access_code_alert_message), + confirmText = resourceReference(R.string.common_disable), + onConfirm = { onDisable() }, + onDismiss = onDismiss, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 47bbb8376f..bde3cf116f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item import com.tangem.wallet.R @@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory { ) } + fun createUseBiometricsSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = ID_USE_BIOMETRICS_SWITCH, + title = resourceReference(R.string.app_settings_enable_biometrics_title), + description = resourceReference( + R.string.app_settings_biometrics_footer, + wrappedList(resourceReference(R.string.common_biometrics)), + ), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createRequireAccessCodeSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = ID_REQUIRE_ACCESS_CODE_SWITCH, + title = resourceReference(R.string.app_settings_require_access_code), + description = resourceReference(R.string.app_settings_require_access_code_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + fun createSaveAccessCodeSwitch( isChecked: Boolean, isEnabled: Boolean, @@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory { const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch" const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button" const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button" + const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch" + const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index ee93849f31..8b9f0e5a01 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor( private val appThemeModeRepository: AppThemeModeRepository, private val settingsRepository: SettingsRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model(), StoreSubscriber { private val itemsFactory = AppSettingsItemsFactory() @@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor( onClick = ::showAppCurrencySelector, ).let(::add) - if (state.isBiometricsAvailable) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress - itemsFactory.createSaveWalletsSwitch( - isChecked = state.saveWallets, + itemsFactory.createUseBiometricsSwitch( + isChecked = state.useBiometricAuthentication, isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveWalletsToggled, + onCheckedChange = ::onBiometricAuthenticationToggled, ).let(::add) - itemsFactory.createSaveAccessCodeSwitch( - isChecked = state.saveAccessCodes, - isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveAccessCodesToggled, + itemsFactory.createRequireAccessCodeSwitch( + isChecked = state.requireAccessCode, + isEnabled = canUseBiometrics && state.useBiometricAuthentication, + onCheckedChange = ::onRequireAccessCodeToggled, ).let(::add) + } else { + if (state.isBiometricsAvailable) { + val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress + + itemsFactory.createSaveWalletsSwitch( + isChecked = state.saveWallets, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveWalletsToggled, + ).let(::add) + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = state.saveAccessCodes, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveAccessCodesToggled, + ).let(::add) + } } itemsFactory.createFlipToHideBalanceSwitch( @@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor( } } + private fun onBiometricAuthenticationToggled(isChecked: Boolean) { + // TODO : Uncomment and implement analytics event when ready + // val param = AnalyticsParam.OnOffState(isChecked) + // analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param)) + if (isChecked) { + onSettingsToggled(AppSetting.BiometricAuthentication, enable = true) + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDisableBiometricAuthenticationAlert( + onDisable = { + onSettingsToggled(AppSetting.BiometricAuthentication, enable = false) + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onRequireAccessCodeToggled(isChecked: Boolean) { + // TODO : Uncomment and implement analytics event when ready + // val param = AnalyticsParam.OnOffState(isChecked) + // analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param)) + updateContentState { + copy( + dialog = if (isChecked) { + dialogsFactory.createEnableRequireAccessCodeAlert( + onEnable = { + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ) + } else { + dialogsFactory.createDisableRequireAccessCodeAlert( + onDisable = { + onSettingsToggled(AppSetting.RequireAccessCode, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ) + }, + ) + } + } + private fun onSaveWalletsToggled(isChecked: Boolean) { if (isChecked) { onSettingsToggled(AppSetting.SaveWallets, enable = true) @@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor( saveWallets = walletsRepository.shouldSaveUserWalletsSync(), saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), isBiometricsAvailable = canUseBiometryUseCase(), + useBiometricAuthentication = walletsRepository.useBiometricAuthentication(), + requireAccessCode = walletsRepository.requireAccessCode(), isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index e71a54fcdd..1d5b58407d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -87,9 +87,10 @@ internal class CardSettingsModel @Inject constructor( val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet $userWalletId not found") } + .requireColdWallet() cardSdkConfigRepository.isBiometricsRequestPolicy = - userWallet.requireColdWallet().scanResponse.card.isAccessCodeSet && // TODO [REDACTED_TASK_KEY] + userWallet.scanResponse.card.isAccessCodeSet && settingsRepository.shouldSaveAccessCodes() } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt index 34e073bf69..2d0af3ca0b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt @@ -268,7 +268,7 @@ internal class ResetCardModel @Inject constructor( if (isLocked && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { popTo() } } else { - store.dispatchNavigationAction { replaceAll(AppRoute.Home) } + store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt deleted file mode 100644 index b89b6f5255..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.tap.features.home - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.SystemBarsIconsDisposable -import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect -import com.tangem.core.ui.utils.findActivity -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.home.api.HomeComponent -import com.tangem.tap.features.home.compose.StoriesScreen -import com.tangem.tap.features.home.compose.StoriesScreenV2 -import com.tangem.tap.features.home.redux.HomeAction -import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.store -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import org.rekotlin.StoreSubscriber - -@Suppress("UnusedPrivateMember") -internal class DefaultHomeComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, -) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber { - - private val model: HomeModel = getOrCreateModel() - - private var homeState: MutableState = mutableStateOf(store.state.homeState) - - init { - lifecycle.subscribe( - onCreate = { - store.dispatch(HomeAction.OnCreate) - }, - onStart = { - store.subscribe(subscriber = this) { state -> - state - .skipRepeats { oldState, newState -> oldState.homeState == newState.homeState } - .select(AppState::homeState) - } - }, - onStop = { - store.unsubscribe(this) - }, - ) - } - - @Composable - override fun Content(modifier: Modifier) { - val activity = LocalContext.current.findActivity() - BackHandler(onBack = activity::finish) - SystemBarsIconsDisposable(darkIcons = false) - if (hotWalletFeatureToggles.isHotWalletEnabled) { - StoriesScreenV2( - homeState = homeState, - onCreateNewWalletButtonClick = model::onCreateNewWalletScreen, - onAddExistingWalletButtonClick = model::onAddExistingWalletScreen, - onScanButtonClick = model::onScanClick, - ) - } else { - StoriesScreen( - homeState = homeState, - onScanButtonClick = model::onScanClick, - onShopButtonClick = model::onShopClick, - onSearchTokensClick = model::onSearchClick, - ) - } - - ChangeRootBackgroundColorEffect(Color(color = 0xFF010101)) - } - - override fun newState(state: HomeState) { - homeState.value = state - } - - @AssistedFactory - interface Factory : HomeComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultHomeComponent - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt deleted file mode 100644 index 6110c7ff0c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.tangem.tap.features.home - -import androidx.compose.runtime.Stable -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRoute.ManageTokens.Source -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.tokens.TokensAction -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -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.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY -import com.tangem.tap.features.home.redux.HomeAction -import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL -import com.tangem.tap.store -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.Locale -import javax.inject.Inject - -@Suppress("LongParameterList") -@Stable -@ModelScoped -internal class HomeModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val scanCardProcessor: ScanCardProcessor, - private val saveWalletUseCase: SaveWalletUseCase, - private val cardSdkConfigRepository: CardSdkConfigRepository, - private val settingsRepository: SettingsRepository, - private val urlOpener: UrlOpener, - private val analyticsEventHandler: AnalyticsEventHandler, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val router: Router, - getUserCountryUseCase: GetUserCountryUseCase, -) : Model() { - - private val tangemErrorHandler = TangemTangemErrorsHandler(store) - - init { - getUserCountryUseCase.invoke() - .distinctUntilChanged() - .filterNotNull() - .onEach { - val userCountry = it.getOrNull() ?: UserCountry.Other(Locale.getDefault().country) - store.dispatchOnMain(HomeAction.UserCountryLoaded(userCountry)) - } - .flowOn(dispatchers.io) - .launchIn(modelScope) - } - - fun onCreateNewWalletScreen() { - router.push(AppRoute.CreateWalletSelection) - } - - fun onAddExistingWalletScreen() { - router.push(AppRoute.AddExistingWallet) - } - - fun onScanClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) - scanCard() - } - - fun onShopClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) - analyticsEventHandler.send(Shop.ScreenOpened()) - - 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(Source.STORIES)) } - } - - private fun scanCard() { - modelScope.launch { - cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes() - - scanCardProcessor.scan( - analyticsSource = AnalyticsParam.ScreensSources.Intro, - onProgressStateChange = { showProgress -> - if (showProgress) { - store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) - } else { - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - } - }, - onFailure = { - tangemErrorHandler.onErrorReceived(error = it) - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - }, - onSuccess = ::proceedWithScanResponse, - ) - } - } - - private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { - val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() - - if (userWallet == null) { - Timber.e("User wallet not created") - return - } - - saveWalletUseCase(userWallet).fold( - ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") }, - ifRight = { - sendSignedInCardAnalyticsEvent(scanResponse) - coroutineScope { store.onUserWalletSelected(userWallet = userWallet) } - }, - ) - - store.dispatchWithMain(HomeAction.ScanInProgress(scanInProgress = false)) - delay(HIDE_PROGRESS_DELAY) - - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - } - - private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { - val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) - - if (currency != null) { - Analytics.send( - event = Basic.SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = Basic.SignedIn.SignInType.Card, - walletsCount = "1", - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt b/app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt deleted file mode 100644 index fb4ae25b2b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.features.home - -import androidx.compose.ui.text.intl.Locale - -/** -[REDACTED_AUTHOR] - */ -interface RegionProvider { - fun getRegion(): String? -} - -class LocaleRegionProvider : RegionProvider { - override fun getRegion(): String = Locale.current.region -} - -const val RUSSIA_COUNTRY_CODE = "ru" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt b/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt deleted file mode 100644 index 40e51a8de8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -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) : 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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt deleted file mode 100644 index 0c63e10a1c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.features.home.api - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent - -interface HomeComponent : ComposableContentComponent { - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt b/app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt deleted file mode 100644 index 9f92cc5583..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.home.di - -import com.tangem.core.decompose.model.Model -import com.tangem.tap.features.home.DefaultHomeComponent -import com.tangem.tap.features.home.HomeModel -import com.tangem.tap.features.home.api.HomeComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(SingletonComponent::class) -internal interface HomeFeatureModule { - - @Binds - fun bindFactory(impl: DefaultHomeComponent.Factory): HomeComponent.Factory - - @Binds - @IntoMap - @ClassKey(HomeModel::class) - fun bindModel(model: HomeModel): Model -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt b/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt deleted file mode 100644 index 7946b7ec5e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.features.home.errors - -import com.tangem.common.core.TangemError - -interface TangemSdkErrorHandler { - - fun onErrorReceived(error: TangemError) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt deleted file mode 100644 index 9a449e938e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.home.redux - -import com.tangem.domain.settings.usercountry.models.UserCountry -import kotlinx.coroutines.CoroutineScope -import org.rekotlin.Action - -sealed class HomeAction : Action { - - data object OnCreate : HomeAction() - - /** - * Action for scanning card - * - * @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed - */ - data class ReadCard(val scope: CoroutineScope) : HomeAction() - - data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() - - data class UserCountryLoaded(val userCountry: UserCountry) : HomeAction() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt deleted file mode 100644 index e66b7f8c84..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.tap.features.home.redux - -import android.content.res.Resources -import com.tangem.common.doOnFailure -import com.tangem.common.doOnResult -import com.tangem.common.doOnSuccess -import com.tangem.common.extensions.guard -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter -import com.tangem.tap.common.analytics.events.IntroductionProcess -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.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware -import timber.log.Timber -import java.util.Locale - -internal const val HIDE_PROGRESS_DELAY = 400L - -object HomeMiddleware { - val handler = homeMiddleware - - private val SYSTEM_LANGUAGE = - runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" } - private val APP_LANGUAGE = Locale.getDefault().language - private val UTM_MARKS = "utm_source=tangem-app" + - "&utm_medium=app" + - "&utm_campaign=prospect-$SYSTEM_LANGUAGE" + - "&utm_content=devicelang-$APP_LANGUAGE" - - val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?$UTM_MARKS" -} - -private val homeMiddleware: Middleware = { _, _ -> - { next -> - { action -> - handleHomeAction(action) - next(action) - } - } -} - -private fun handleHomeAction(action: Action) { - when (action) { - is HomeAction.OnCreate -> { - Analytics.eraseContext() - Analytics.send(IntroductionProcess.ScreenOpened()) - - store.dispatch(GlobalAction.RestoreAppCurrency) - } - is HomeAction.ReadCard -> { - action.scope.launch { - readCard() - } - } - } -} - -private suspend fun readCard() { - val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = shouldSaveAccessCodes, - ) - - store.inject(DaggerGraphState::scanCardProcessor).scan( - analyticsSource = AnalyticsParam.ScreensSources.Intro, - onProgressStateChange = { showProgress -> - if (showProgress) { - store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) - } else { - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - } - }, - onFailure = { - Timber.e(it, "Unable to scan card") - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - }, - onSuccess = { scanResponse -> - proceedWithScanResponse(scanResponse) - }, - ) -} - -private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { - val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse) - - val userWallet = userWalletBuilder.build().guard { - Timber.e("User wallet not created") - return@launch - } - - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.save(userWallet) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - } - .doOnSuccess { - sendSignedInCardAnalyticsEvent(scanResponse) - store.onUserWalletSelected(userWallet = userWallet) - } - .doOnResult { - navigateTo(AppRoute.Wallet) - } -} - -private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { - val currency = ParamCardCurrencyConverter().convert( - value = scanResponse.cardTypesResolver, - ) - - if (currency != null) { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - Analytics.send( - event = Basic.SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = Basic.SignedIn.SignInType.Card, - walletsCount = userWalletsListManager.walletsCount.toString(), - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } -} - -private suspend fun navigateTo(route: AppRoute) { - store.dispatchNavigationAction { push(route) } - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt deleted file mode 100644 index 678de45d76..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.features.home.redux - -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.tap.common.redux.AppState -import kotlinx.collections.immutable.toImmutableList -import org.rekotlin.Action - -object HomeReducer { - fun reduce(action: Action, state: AppState): HomeState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, appState: AppState): HomeState { - if (action !is HomeAction) return appState.homeState - - return when (action) { - is HomeAction.ScanInProgress -> { - appState.homeState.copy(scanInProgress = action.scanInProgress) - } - is HomeAction.UserCountryLoaded -> { - val stories = if (action.userCountry.needApplyFCARestrictions()) { - getRestrictedStories() - } else { - Stories.entries - } - appState.homeState.copy( - stories = stories.toImmutableList(), - ) - } - else -> appState.homeState - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt b/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt deleted file mode 100644 index b9d1c6ae6c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.features.hot - -import com.tangem.hot.sdk.model.HotAuth -import com.tangem.hot.sdk.model.HotWalletId -import com.tangem.tap.domain.hot.HotWalletPasswordRequester -import javax.inject.Inject - -class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester { - - override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password { - return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY] - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt index 9cc8db4191..af00ff8af8 100644 --- a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt +++ b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt @@ -37,6 +37,9 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk { override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId = callSdk { changeAuth(unlockHotWallet, auth) } + override suspend fun removeBiometryAuthIfPresented(id: HotWalletId): HotWalletId = + callSdk { removeBiometryAuthIfPresented(id) } + override suspend fun derivePublicKey( unlockHotWallet: UnlockHotWallet, request: DeriveWalletRequest, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 90ef042a6b..766392b9dd 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -4,21 +4,12 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.features.home.redux.HomeAction -import com.tangem.tap.features.intentHandler.IntentHandler -import com.tangem.tap.features.intentHandler.AffectsNavigation -import com.tangem.tap.features.welcome.redux.WelcomeAction -import com.tangem.tap.store -import kotlinx.coroutines.CoroutineScope +import com.tangem.common.routing.entity.InitScreenLaunchMode /** [REDACTED_AUTHOR] */ -class BackgroundScanIntentHandler( - private val hasSavedUserWalletsProvider: () -> Boolean, - private val scope: CoroutineScope, -) : IntentHandler, AffectsNavigation { +class BackgroundScanIntentHandler { private val nfcActions = arrayOf( NfcAdapter.ACTION_NDEF_DISCOVERED, @@ -26,8 +17,15 @@ class BackgroundScanIntentHandler( NfcAdapter.ACTION_TAG_DISCOVERED, ) - override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { - if (isFromForeground) return true + fun getInitScreenLaunchMode(intent: Intent?): InitScreenLaunchMode { + return if (shouldOpenScanCard(intent)) { + InitScreenLaunchMode.WithCardScan + } else { + InitScreenLaunchMode.Standard + } + } + + private fun shouldOpenScanCard(intent: Intent?): Boolean { if (intent == null || intent.action !in nfcActions) return false val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -36,15 +34,9 @@ class BackgroundScanIntentHandler( @Suppress("DEPRECATION") intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) } - if (tag == null) return false intent.action = null - if (hasSavedUserWalletsProvider.invoke()) { - store.dispatchOnMain(WelcomeAction.ProceedWithCard) - } else { - store.dispatchOnMain(HomeAction.ReadCard(scope = scope)) - } - return true + return tag != null } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index 6f138c18e5..e715290a1e 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -4,8 +4,8 @@ import android.content.Intent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.intentHandler.AffectsNavigation +import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.store import timber.log.Timber import java.net.URLDecoder diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 6b5c655b9e..643710e4c2 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -24,8 +24,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.common.LogConfig -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.models.ApplicationId @@ -212,15 +210,11 @@ internal class MainViewModel @Inject constructor( } private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService { - val cardProvider: () -> ScanResponse? = { - userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY] - } - return MoonPayService( apiKey = environmentConfig.moonPayApiKey, secretKey = environmentConfig.moonPayApiSecretKey, logEnabled = LogConfig.network.moonPayService, - cardProvider = { cardProvider.invoke()?.card }, + userWalletProvider = { userWalletsListManager.selectedUserWalletSync }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt index 07a92373e1..215560a1ad 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.component +import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent @@ -7,6 +8,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { data class Params( + val launchMode: InitScreenLaunchMode, val intent: SerializableIntent?, ) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index 8b1dc10be1..5965f011f3 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.welcome.model import com.tangem.common.core.TangemError +import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -44,10 +45,9 @@ internal class WelcomeModel @Inject constructor( subscribeToStoreChanges() initGlobalState() - val welcomeAction = if (params.intent != null) { - WelcomeAction.ProceedWithIntent(params.intent.toIntent()) - } else { - WelcomeAction.ProceedWithBiometrics() + val welcomeAction = when (params.launchMode) { + is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard + is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics(params.intent?.toIntent()) } store.dispatch(welcomeAction) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index ebbc8b2fc6..8a842072d8 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -11,19 +11,17 @@ import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.proxy.redux.DaggerGraphState -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -44,7 +42,7 @@ internal class WelcomeMiddleware { private fun handleAction(action: WelcomeAction, state: WelcomeState) { mainScope.launch { when (action) { - is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this) + is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent) is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics( afterUnlockIntent = action.afterUnlockIntent ?: state.intent, ) @@ -55,7 +53,7 @@ internal class WelcomeMiddleware { } } - private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) { + private suspend fun proceedWithIntent(initialIntent: Intent) { Timber.d( """ Proceeding with intent @@ -63,15 +61,12 @@ internal class WelcomeMiddleware { """.trimIndent(), ) - val handler = BackgroundScanIntentHandler( - scope = scope, - hasSavedUserWalletsProvider = { true }, - ) - val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false) val hasUncompletedBackup = backupService.hasIncompletedBackup - if (!isBackgroundScanHandled && !hasUncompletedBackup) { + if (!hasUncompletedBackup) { store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent)) + } else { + store.dispatchWithMain(WelcomeAction.ProceedWithCard) } } @@ -139,7 +134,7 @@ internal class WelcomeMiddleware { } private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) { - // TODO [REDACTED_TASK_KEY] + // TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics if (userWallet !is UserWallet.Cold) { return diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 5ce4ad3c20..99425e0191 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -4,11 +4,16 @@ import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository -internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider { +internal class DefaultAuthProvider( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean = false, +) : AuthProvider { - override fun getCardPublicKey(): String { - val userWallet = userWalletsListManager.selectedUserWalletSync + override suspend fun getCardPublicKey(): String { + val userWallet = getSelectedWallet() if (userWallet !is UserWallet.Cold) { return "" @@ -17,8 +22,8 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle return userWallet.scanResponse.card.cardPublicKey.toHexString() } - override fun getCardId(): String { - val userWallet = userWalletsListManager.selectedUserWalletSync + override suspend fun getCardId(): String { + val userWallet = getSelectedWallet() if (userWallet !is UserWallet.Cold) { return "" @@ -27,9 +32,25 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle return userWallet.scanResponse.card.cardId } - override fun getCardsPublicKeys(): Map { - return userWalletsListManager.userWalletsSync.filterIsInstance().associate { + override suspend fun getCardsPublicKeys(): Map { + return getWallets().filterIsInstance().associate { it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() } } + + private suspend fun getWallets(): List { + return if (useNewListRepository) { + userWalletsListRepository.userWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } + } + + private suspend fun getSelectedWallet(): UserWallet? { + return if (useNewListRepository) { + userWalletsListRepository.selectedUserWalletSync() + } else { + userWalletsListManager.selectedUserWalletSync + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 5f724624af..95004c11d8 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -3,6 +3,8 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider @@ -22,8 +24,16 @@ internal class AuthModule { @Provides @Singleton - fun provideAuthProvider(userWalletsListManager: UserWalletsListManager): AuthProvider { - return DefaultAuthProvider(userWalletsListManager) + fun provideAuthProvider( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): AuthProvider { + return DefaultAuthProvider( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 6b10fa0d44..6c4c20e0cc 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -13,9 +13,9 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index f57f247e74..02bc4321ae 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -12,7 +12,7 @@ import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus @@ -27,7 +27,7 @@ class MoonPayService( private val apiKey: String, private val secretKey: String, private val logEnabled: Boolean, - private val cardProvider: () -> CardDTO?, + private val userWalletProvider: () -> UserWallet?, ) : ExchangeService { override val initializationStatus: StateFlow @@ -103,8 +103,8 @@ class MoonPayService( } override fun availableForSell(currency: Currency): Boolean { - val card = cardProvider() ?: return false - val checkCardExchange = !card.isStart2Coin + val userWallet = userWalletProvider() ?: return false + val checkCardExchange = userWallet !is UserWallet.Cold || !userWallet.scanResponse.card.isStart2Coin if (!checkCardExchange) return false diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index 235cf6be4c..14757eae60 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -158,4 +158,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? ZkLinkNova, ZkLinkNovaTestnet -> null KaspaTestnet -> null Pepecoin, PepecoinTestnet -> null + Hyperliquid, HyperliquidTestnet -> null } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 1c696ac842..e0b2794648 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -21,6 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -31,7 +32,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.hot.sdk.TangemHotSdk import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository @@ -77,4 +80,7 @@ data class DaggerGraphState( val cardArworksProvider: CardArtworksProvider? = null, val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, val userTokensResponseStore: UserTokensResponseStore? = null, + val userWalletsListRepository: UserWalletsListRepository? = null, + val hotWalletFeatureToggles: HotWalletFeatureToggles? = null, + val tangemHotSdk: TangemHotSdk? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 82979df7ee..615b1c3778 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -8,12 +8,20 @@ import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent -import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -31,6 +39,7 @@ import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.v2.api.SendWithSwapComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -42,12 +51,12 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCo import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent -import com.tangem.tap.features.home.api.HomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent +import com.tangem.features.welcome.WelcomeComponent as NewWelcomeComponent @ActivityScoped @Suppress("LongParameterList", "LargeClass") @@ -66,6 +75,7 @@ internal class ChildFactory @Inject constructor( private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory, private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, private val welcomeComponentFactory: WelcomeComponent.Factory, + private val newWelcomeComponentFactory: NewWelcomeComponent.Factory, private val storiesComponentFactory: StoriesComponent.Factory, private val stakingComponentFactory: StakingComponent.Factory, private val swapComponentFactory: SwapComponent.Factory, @@ -84,6 +94,9 @@ internal class ChildFactory @Inject constructor( private val walletComponentFactory: WalletEntryComponent.Factory, private val sendComponentFactoryV2: SendComponent.Factory, private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory, + private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory, + private val accountDetailsComponentFactory: AccountDetailsComponent.Factory, + private val archivedAccountListComponentFactory: ArchivedAccountListComponent.Factory, private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, @@ -91,9 +104,14 @@ internal class ChildFactory @Inject constructor( private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, + private val walletActivationComponentFactory: WalletActivationComponent.Factory, + private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, + private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, + private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -130,13 +148,22 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.Welcome -> { - createComponentChild( - context = context, - params = WelcomeComponent.Params( - intent = route.intent, - ), - componentFactory = welcomeComponentFactory, - ) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + createComponentChild( + context = context, + params = Unit, + componentFactory = newWelcomeComponentFactory, + ) + } else { + createComponentChild( + context = context, + params = WelcomeComponent.Params( + launchMode = route.launchMode, + intent = route.intent, + ), + componentFactory = welcomeComponentFactory, + ) + } } is AppRoute.WalletSettings -> { createComponentChild( @@ -290,7 +317,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Home -> { createComponentChild( context = context, - params = Unit, + params = HomeComponent.Params(route.launchMode), componentFactory = homeComponentFactory, ) } @@ -385,6 +412,7 @@ internal class ChildFactory @Inject constructor( params = PushNotificationsParams( modelCallbacks = PushNotificationsModelCallbacksStub(), source = route.source, + nextRoute = AppRoute.Home(), ), componentFactory = pushNotificationsComponentFactory, ) @@ -438,6 +466,8 @@ internal class ChildFactory @Inject constructor( initialCurrency = route.initialCurrency, selectedCurrency = route.selectedCurrency, source = ChooseManagedTokensComponent.Source.valueOf(route.source.name), + showSendViaSwapNotification = route.showSendViaSwapNotification, + analyticsCategoryName = route.analyticsCategoryName, ), componentFactory = chooseManagedTokensComponentFactory, ) @@ -463,6 +493,33 @@ internal class ChildFactory @Inject constructor( componentFactory = addExistingWalletComponentFactory, ) } + is AppRoute.WalletActivation -> { + createComponentChild( + context = context, + params = WalletActivationComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = walletActivationComponentFactory, + ) + } + is AppRoute.CreateWalletBackup -> { + createComponentChild( + context = context, + params = CreateWalletBackupComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = createWalletBackupComponentFactory, + ) + } + is AppRoute.UpdateAccessCode -> { + createComponentChild( + context = context, + params = UpdateAccessCodeComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = updateAccessCodeComponentFactory, + ) + } is AppRoute.SendEntryPoint -> { createComponentChild( context = context, @@ -483,6 +540,49 @@ internal class ChildFactory @Inject constructor( componentFactory = sendWithSwapComponentFactory, ) } + is AppRoute.CreateAccount -> { + createComponentChild( + context = context, + params = AccountCreateEditComponent.Params.Create( + userWalletId = route.userWalletId, + ), + componentFactory = accountCreateEditComponentFactory, + ) + } + is AppRoute.EditAccount -> { + createComponentChild( + context = context, + params = AccountCreateEditComponent.Params.Edit( + account = route.account, + ), + componentFactory = accountCreateEditComponentFactory, + ) + } + is AppRoute.AccountDetails -> { + createComponentChild( + context = context, + params = AccountDetailsComponent.Params( + account = route.account, + ), + componentFactory = accountDetailsComponentFactory, + ) + } + is AppRoute.ArchivedAccountList -> { + createComponentChild( + context = context, + params = ArchivedAccountListComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = archivedAccountListComponentFactory, + ) + } + is AppRoute.TangemPayDetails -> { + createComponentChild( + context = context, + params = TangemPayDetailsComponent.Params(), + componentFactory = tangemPayDetailsComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4cbeef6686..c490aef8c2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,24 +96,21 @@ val generateComposeMetrics by tasks.registering { description = "Build external APK and generates compose metrics to 'build/compose-metrics' directory" subprojects { - tasks.withType { - compilerOptions { + tasks.withType().configureEach { + if (name.contains("compile")) { val outputDirectory = "${project.buildDir.absolutePath}/compose_metrics" - // Metrics - freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$outputDirectory", - ) - // Reports - freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory", - ) - // Compose strong skipping mode - // freeCompilerArgs.addAll( - // "-P", - // "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", - // ) + compilerOptions { + freeCompilerArgs.addAll( + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$outputDirectory", + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory" + // "-P", + // "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", + ) + ) + } } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 682378bf51..d0ffcca403 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -4,11 +4,13 @@ import android.os.Bundle import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle +import com.tangem.common.routing.entity.InitScreenLaunchMode 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.feedback.models.CardInfo import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId @@ -23,10 +25,16 @@ sealed class AppRoute(val path: String) : Route { data object Initial : AppRoute(path = "/initial") @Serializable - data object Home : AppRoute(path = "/home") + data class Home( + val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, + ) : AppRoute(path = "/home") @Serializable data class Welcome( + @Deprecated("No longer used, will be removed in future releases") + val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, + // we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware + @Deprecated("No longer used, will be removed in future releases") val intent: SerializableIntent? = null, ) : AppRoute(path = "/welcome"), RouteBundleParams { @@ -129,6 +137,8 @@ sealed class AppRoute(val path: String) : Route { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val source: Source, + val showSendViaSwapNotification: Boolean, + val analyticsCategoryName: String, ) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") { enum class Source { SendViaSwap, @@ -304,6 +314,21 @@ sealed class AppRoute(val path: String) : Route { @Serializable object AddExistingWallet : AppRoute(path = "/add_existing_wallet") + @Serializable + data class WalletActivation( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}") + + @Serializable + data class CreateWalletBackup( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") + + @Serializable + data class UpdateAccessCode( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}") + @Serializable data class SendEntryPoint( val userWalletId: UserWalletId, @@ -317,4 +342,27 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, val currency: CryptoCurrency, ) : AppRoute(path = "/send_with_swap/${userWalletId.stringValue}/${currency.symbol}") + + @Serializable + data class CreateAccount( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/create_account/${userWalletId.stringValue}") + + @Serializable + data class EditAccount( + val account: Account, + ) : AppRoute(path = "/edit_account/${account.accountId.value}") + + @Serializable + data class AccountDetails( + val account: Account, + ) : AppRoute(path = "/account_details/${account.accountId.value}") + + @Serializable + data class ArchivedAccountList( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/archived_account/${userWalletId.stringValue}") + + @Serializable + data object TangemPayDetails : AppRoute(path = "/tangem_pay_details") } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt new file mode 100644 index 0000000000..3e1e008994 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt @@ -0,0 +1,13 @@ +package com.tangem.common.routing.entity + +import kotlinx.serialization.Serializable + +@Serializable +sealed class InitScreenLaunchMode { + + @Serializable + data object Standard : InitScreenLaunchMode() + + @Serializable + data object WithCardScan : InitScreenLaunchMode() +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt index 38ef6f2096..cb4f2b871a 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt @@ -14,6 +14,7 @@ data class SerializableIntent( val packageValue: String?, val component: String?, val flags: Int, + // CAUTION: works wrong with SerializableBundle constructor(bundle: Bundle), need to be removed val extras: SerializableBundle?, ) { diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt index 1616a4c847..bb2343344e 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt @@ -5,7 +5,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import java.math.BigDecimal /** diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt index 189705d948..c58ff8edc0 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import java.math.BigDecimal /** diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index 6592071c92..c56e3718ef 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -8,8 +8,8 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.card.DerivationStyleProvider -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -25,6 +25,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul val cardano by lazy { createCoin(blockchain = Blockchain.Cardano) } val chia by lazy { createCoin(Blockchain.Chia) } val ethereum by lazy { createCoin(Blockchain.Ethereum) } + val stellar by lazy { createCoin(Blockchain.Stellar) } val chiaAndEthereum by lazy { listOf( @@ -64,6 +65,10 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS + else -> Network.NameResolvingType.NONE + }, ) return factory.createCoin(network = network) @@ -94,6 +99,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "NEVER-MIND", symbol = "NEVER-MIND", diff --git a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt index c8a88ac0ff..67cd37711b 100644 --- a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt @@ -9,7 +9,24 @@ fun assertEither(actual: Either, expected: Either) { + actual + .onRight { Truth.assertThat(actual).isEqualTo(Either.Right(Unit)) } + .onLeft { + error("Actual is Either.Left: $it") + } +} + +fun assertEitherLeft(actual: Either, expected: Throwable) { + actual + .onRight { error("Actual is Either.Right: $it") } + .onLeft { + Truth.assertThat(it::class.java).isEqualTo(expected::class.java) + Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message) + } } \ No newline at end of file diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index f6f607f75a..2cb8f794be 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { /** Compose */ implementation(deps.compose.material3) - implementation(deps.compose.material) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt index cdacdd3ce7..7406013c29 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -4,7 +4,7 @@ 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.amountScreen.models.AmountState -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index e52898094a..244ea5b4c0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index 7e5cf0ab8d..cb91187d95 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index a38f516c70..2b60066ea7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -16,8 +17,9 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf @@ -58,6 +60,7 @@ class AmountStateConverter( return AmountState.Data( title = value.title, availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + availableBalanceShort = stringReference(crypto), tokenName = stringReference(status.currency.name), tokenIconState = iconStateConverter.convert(status), amountTextField = amountFieldConverter.convert(value.value), @@ -120,10 +123,15 @@ class AmountStateConverterV2( return AmountState.Data( title = value.title, availableBalance = if (isRedesignEnabled) { - resourceReference(R.string.common_balance, wrappedList(crypto)) + combinedReference( + stringReference(crypto), + stringReference(" $DOT "), + stringReference(fiat), + ) } else { resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) }, + availableBalanceShort = stringReference(crypto), tokenName = stringReference(cryptoCurrencyStatus.currency.name), tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency), amountTextField = amountFieldConverter.convert(value.value), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt index dce1957361..045c704421 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt @@ -1,7 +1,7 @@ package com.tangem.common.ui.amountScreen.converters import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index a689b4501a..7385fa9542 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -3,13 +3,16 @@ package com.tangem.common.ui.amountScreen.converters.field import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.transformer.Transformer /** @@ -34,13 +37,18 @@ class AmountBoundaryUpdateTransformer( val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } val availableBalance = if (isRedesignEnabled) { - resourceReference(R.string.common_balance, wrappedList(crypto)) + combinedReference( + stringReference(crypto), + stringReference(" $DOT "), + stringReference(fiat), + ) } else { resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) } return prevState.copy( availableBalance = availableBalance, + availableBalanceShort = stringReference(crypto), ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index cbfc6907e8..4a490037ae 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index f19fad6bb4..9c42eeac7e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -9,9 +9,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.convertToAmount import com.tangem.utils.Provider import com.tangem.utils.converter.Converter diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index 72dcac8ac9..a8c20c0745 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.extensions.isZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index cc23669f47..cac4628783 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -17,7 +17,8 @@ sealed class AmountState { /** * @param isPrimaryButtonEnabled indicates if next state button enabled * @param title title - * @param availableBalance user crypto currency balance + * @param availableBalance user crypto currency balance with fiat balance + * @param availableBalanceShort user crypto currency balance without fiat balance * @param tokenIconState crypto currency icon state * @param segmentedButtonConfig currency switcher config * @param selectedButton selected currency index @@ -33,6 +34,7 @@ sealed class AmountState { override val isRedesignEnabled: Boolean, val title: TextReference, val availableBalance: TextReference, + val availableBalanceShort: TextReference, val tokenName: TextReference, val tokenIconState: CurrencyIconState, val segmentedButtonConfig: PersistentList, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 99c11a9a14..a2db25c4dd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -24,7 +24,8 @@ object AmountStatePreviewData { val amountState = AmountState.Data( isPrimaryButtonEnabled = false, title = stringReference("Family Wallet"), - availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), + availableBalance = stringReference("2 130,88 USDT • 2 129,92 \$)"), + availableBalanceShort = stringReference("2 130,88 USDT"), tokenIconState = CurrencyIconState.Loading, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index e4f345ca11..47418a9338 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -25,6 +26,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import java.math.BigDecimal @Composable @@ -63,7 +65,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis maxLines = 1, modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing24), + .padding(top = TangemTheme.dimens.spacing24) + .testTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT), ) Text( text = secondAmount, @@ -72,7 +75,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing8), + .padding(top = TangemTheme.dimens.spacing8) + .testTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index b9fa864b84..979a1a4419 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState @@ -24,6 +25,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -43,7 +45,7 @@ fun AmountBlockV2( crypto( symbol = "", decimals = amount.cryptoAmount.decimals, - ) + ).uncapped() }.orEmpty() val fiatAmount = amount.fiatAmount.value.format { @@ -62,7 +64,7 @@ fun AmountBlockV2( AmountBlockV2( title = amountState.title, - balance = amountState.availableBalance, + balance = amountState.availableBalanceShort, currencyTitle = currencyTitle, currencyIconState = amountState.tokenIconState, firstAmount = firstAmount, @@ -102,7 +104,7 @@ private fun AmountBlockV2( Row { Text( text = title.resolveReference(), - style = TangemTheme.typography.body2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) SpacerWMax() @@ -121,18 +123,20 @@ private fun AmountBlockV2( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 8.dp), ) { - Text( + ResizableText( text = firstAmount, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, + maxLines = 1, ) Row( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( + ResizableText( text = secondAmount, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + maxLines = 1, ) extraContent() } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index f1d8e921a8..753a0fc99e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import kotlinx.collections.immutable.PersistentList private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" @@ -77,7 +79,8 @@ internal fun LazyListScope.buttons( .padding( vertical = TangemTheme.dimens.spacing10, horizontal = TangemTheme.dimens.spacing34, - ), + ) + .testTag(StakingSendScreenTestTags.MAX_BUTTON), ) } } @@ -90,7 +93,8 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment .fillMaxSize() .padding( horizontal = TangemTheme.dimens.spacing10, - ), + ) + .testTag(StakingSendScreenTestTags.CURRENCY_BUTTON), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { @@ -102,13 +106,13 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment url = button.iconUrl, size = TangemTheme.dimens.size18, isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier, + modifier = iconModifier.testTag(StakingSendScreenTestTags.FIAT_ICON), ) } else if (button.iconState != null) { CurrencyIcon( state = button.iconState, shouldDisplayNetwork = false, - modifier = iconModifier, + modifier = iconModifier.testTag(StakingSendScreenTestTags.CURRENCY_ICON), ) } Text( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 35d49a5387..f491629420 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import com.tangem.common.ui.amountScreen.models.AmountFieldModel @@ -28,6 +29,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay @@ -116,7 +118,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri textAlign = TextAlign.Center, modifier = Modifier .align(TopCenter) - .padding(bottom = TangemTheme.dimens.spacing32), + .padding(bottom = TangemTheme.dimens.spacing32) + .testTag(StakingSendScreenTestTags.SECONDARY_AMOUNT), ) AmountFieldError( isError = amountField.isError, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index a1ba4366f4..ea0473e930 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.R @@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags private const val AMOUNT_FIELD_KEY = "amountFieldKey" @@ -52,7 +54,8 @@ internal fun LazyListScope.amountField( style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14), + .padding(top = TangemTheme.dimens.spacing14) + .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE), ) val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() @@ -66,7 +69,8 @@ internal fun LazyListScope.amountField( color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2), + .padding(top = TangemTheme.dimens.spacing2) + .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT), ) } CurrencyIcon( @@ -113,7 +117,7 @@ internal fun LazyListScope.amountFieldV2( } else { Text( text = amountState.title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) } @@ -164,7 +168,7 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi modifier = Modifier .padding(end = 16.dp) .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.background.secondary) + .background(TangemTheme.colors.button.secondary) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index 184f7ed46e..96635bbc45 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -127,7 +127,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - modifier = Modifier .fillMaxWidth() .animateContentSize() - .padding(top = 8.dp), + .padding(top = 4.dp), ) { if (amountUM is AmountState.Empty) { TextShimmer( @@ -141,6 +141,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - AmountFieldCurrencyInfo( amountUM = amountUM, onCurrencyChange = onCurrencyChange, + ) AmountFieldError( isError = amountUM.amountTextField.isError, @@ -148,7 +149,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - error = amountUM.amountTextField.error, modifier = Modifier .align(BottomCenter) - .padding(top = 20.dp), + .padding(top = 24.dp), ) } } @@ -161,12 +162,13 @@ private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurre horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .align(TopCenter) - .padding(bottom = 20.dp) + .padding(bottom = 16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) }, - ), + ) + .padding(4.dp), ) { val iconRotateState by animateFloatAsState( targetValue = if (amountUM.amountTextField.isFiatValue) ROTATED_DEGREE else INITIAL_DEGREE, @@ -324,6 +326,7 @@ private class AmountFieldV2PreviewProvider : PreviewParameterProvider GiveTxPermissionBottomSheetContent(content = content) diff --git a/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt b/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt index 3f880fa882..b2c89117fe 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt @@ -49,10 +49,10 @@ fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { text = footerText.resolveAnnotatedReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, + color = TangemTheme.colors.text.tertiary, modifier = Modifier .fillMaxWidth() - .padding(12.dp), + .padding(16.dp), ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index b612670c59..b22f5341e4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -15,6 +15,9 @@ 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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -22,18 +25,18 @@ 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.SecondaryButtonIconStart 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.buttons.common.contentColor 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 import com.tangem.core.ui.res.TangemThemePreview -import kotlinx.collections.immutable.ImmutableList +import com.tangem.core.ui.utils.singleEvent +import com.tangem.core.ui.test.StakingSendScreenTestTags @Composable fun NavigationButtonsBlock( @@ -47,7 +50,7 @@ fun NavigationButtonsBlock( modifier = modifier.fillMaxWidth(), ) { InfoText(footerText) - ExtraButtons(state?.extraButtons, state?.txUrl) + DoneButtons(state?.extraButtons) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), @@ -58,9 +61,33 @@ fun NavigationButtonsBlock( } } +@Composable +fun NavigationButtonsBlockV2( + navigationUM: NavigationUM, + modifier: Modifier = Modifier, + footerText: TextReference? = null, +) { + val navigationUM = navigationUM as? NavigationUM.Content + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + InfoText(footerText) + DoneButtons(navigationUM?.secondaryPairButtonsUM) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + PreviousButton(navigationUM?.prevButton) + NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f)) + } + } +} + @Composable fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { val wrappedButton by rememberNavigationButton(primaryButton) + val hapticFeedback = LocalHapticFeedback.current AnimatedContent( targetState = wrappedButton, transitionSpec = { navigationButtonsTransition() }, @@ -83,7 +110,12 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier TangemButton( text = button.textReference.resolveReference(), enabled = button.isEnabled, - onClick = button.onClick, + onClick = { + if (button.isHapticClick) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + button.onClick() + }, showProgress = button.showProgress, colors = color, textStyle = TangemTheme.typography.subtitle1, @@ -116,40 +148,47 @@ private fun PreviousButton(prevButton: NavigationButton?) { .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) .clickable(onClick = button.onClick) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendScreenTestTags.PREVIOUS_BUTTON), ) } } } @Composable -private fun ExtraButtons(extraButtons: ImmutableList?, txUrl: String?) { +fun DoneButtons(pairButtons: Pair?, modifier: Modifier = Modifier) { AnimatedVisibility( - visible = !txUrl.isNullOrBlank() && extraButtons != null, + visible = pairButtons != null, enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()), exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()), label = "Animate show sent state buttons", - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), ) { - val buttons = remember(this) { requireNotNull(extraButtons) } + val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtons) } Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), ) { - buttons.forEach { button -> - val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) } - ?: TangemButtonIconPosition.None - TangemButton( - text = button.textReference.resolveReference(), - icon = icon, - textStyle = TangemTheme.typography.subtitle1, - onClick = rememberHapticFeedback(state = button, onAction = button.onClick), - modifier = Modifier.weight(1f), - enabled = button.isEnabled, - showProgress = false, - colors = TangemButtonsDefaults.secondaryButtonColors, - ) - } + SecondaryButtonIconStart( + text = leftButton.textReference.resolveReference(), + iconResId = requireNotNull(leftButton.iconRes), + onClick = { + singleEvent { + leftButton.onClick() + } + }, + modifier = Modifier.weight(1f), + ) + SecondaryButtonIconStart( + text = rightButton.textReference.resolveReference(), + iconResId = requireNotNull(rightButton.iconRes), + onClick = { + singleEvent { + rightButton.onClick() + } + }, + modifier = Modifier.weight(1f), + ) } } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index e0bddfab66..772c493cdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -2,7 +2,6 @@ package com.tangem.common.ui.navigationButtons import androidx.annotation.DrawableRes import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList sealed class NavigationButtonsState { data object Empty : NavigationButtonsState() @@ -10,7 +9,7 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton?, val prevButton: NavigationButton?, - val extraButtons: ImmutableList, + val extraButtons: Pair?, val txUrl: String? = null, val onTextClick: (String) -> Unit, ) : NavigationButtonsState() diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 9d2e71fe85..c409bb5b95 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -5,29 +5,25 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import kotlinx.collections.immutable.persistentListOf internal object NavigationButtonsPreview { - private val extraButtons = persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = {}, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = {}, - ), + private val extraButtons = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, ) private val prev = NavigationButton( diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt new file mode 100644 index 0000000000..4b770200d0 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt @@ -0,0 +1,11 @@ +package com.tangem.common.ui.notifications + +/** + * NotificationId represents unique identifiers for notifications in the app. + * + * These ids can be used with [ShouldShowNotificationUseCase] and [SetShouldShowNotificationUseCase] + * to check or update the visibility state of notifications. + */ +enum class NotificationId(val key: String) { + SendViaSwapTokenSelectorNotification("SendViaSwapTokenSelectorNotificationKey"), +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index aa0a20a5bb..ff58fd1c80 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -373,5 +373,15 @@ sealed class NotificationUM(val config: NotificationConfig) { formatArgs = wrappedList(rentInfo.exemptionAmount), ), ) + + data class RentExemptionDestination( + private val rentExemptionAmount: BigDecimal, + ) : Error( + title = TextReference.Res(R.string.send_notification_invalid_amount_title), + subtitle = TextReference.Res( + id = R.string.send_notification_invalid_amount_rent_destination, + formatArgs = wrappedList(rentExemptionAmount), + ), + ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 5e3fb48b92..c06f560367 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError @@ -354,7 +354,11 @@ object NotificationsFactory { onReduceClick = onReduceClick, ) is BlockchainSdkError.DestinationTagRequired -> addRequireDestinationTagErrorNotification() - null -> minAdaValue?.let { + is BlockchainSdkError.Solana.DestinationRentExemption -> addRentExemptionDestinationNotification( + rentExemptionAmount = validationError.rentAmount, + ) + null, + -> minAdaValue?.let { add( NotificationUM.Cardano.MinAdaValueCharged( tokenName = cryptoCurrency.name, @@ -439,6 +443,14 @@ object NotificationsFactory { add(NotificationUM.Solana.RentInfo(rentWarning)) } + fun MutableList.addRentExemptionDestinationNotification(rentExemptionAmount: BigDecimal) { + add( + NotificationUM.Solana.RentExemptionDestination( + rentExemptionAmount = rentExemptionAmount, + ), + ) + } + fun MutableList.addHighFeeWarningNotification( enteredAmountValue: BigDecimal, cryptoCurrencyStatus: CryptoCurrencyStatus, diff --git a/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt b/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt index 94b925980a..dede83288b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -29,6 +30,7 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapStoriesScreenTestTags import kotlinx.collections.immutable.persistentListOf private val SubtitleColor = Color(0xFFB0B0B0) @@ -46,7 +48,8 @@ fun SwapStoriesScreen(config: SwapStoriesUM) { Box( modifier = Modifier .fillMaxSize() - .background(TangemColorPalette.Black), + .background(TangemColorPalette.Black) + .testTag(SwapStoriesScreenTestTags.SCREEN_CONTAINER), ) { SubcomposeAsyncImage( modifier = Modifier.fillMaxSize(), diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 1ddbfffe90..9c80a6a1b7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -13,9 +13,9 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -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 diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 704a867b47..31c66358b4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -3,7 +3,9 @@ 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.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardColors import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -22,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.common.ui.R @@ -30,6 +33,9 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -67,8 +73,10 @@ fun UserWalletItem( balance = state.balance, ) + state.label?.let { Label(it) } + when (state.endIcon) { - UserWalletItemUM.EndIcon.None -> {} + UserWalletItemUM.EndIcon.None -> Unit UserWalletItemUM.EndIcon.Arrow -> { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24), @@ -179,6 +187,19 @@ fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modi radius = TangemTheme.dimens.size2, ) } + is UserWalletItemUM.ImageState.MobileWallet -> { + Image( + modifier = Modifier + .size(36.dp) + .background( + color = TangemTheme.colors.field.focused, + shape = RoundedCornerShape(10.dp), + ) + .padding(6.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_mobile_wallet_icon_24), + contentDescription = null, + ) + } is UserWalletItemUM.ImageState.Image -> { val verifiedArtwork = imageState.artwork.verifiedArtwork if (verifiedArtwork != null) { @@ -257,6 +278,18 @@ private fun Preview_UserWalletItem( private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_0".encodeToByteArray()), + name = stringReference("Mobile Wallet"), + information = getInformation(cardCount = 1), + balance = UserWalletItemUM.Balance.Locked, + label = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + isEnabled = true, + onClick = {}, + ), UserWalletItemUM( id = UserWalletId("user_wallet_1".encodeToByteArray()), name = stringReference("My Wallet"), @@ -347,6 +380,18 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { @@ -45,16 +49,38 @@ class UserWalletItemUMConverter( name = stringReference(name), information = getInfo(userWallet = this), balance = getBalanceInfo(userWallet = this), - isEnabled = !isLocked, + isEnabled = isEnabled(userWallet = this), endIcon = endIcon, onClick = { onClick(value.walletId) }, - imageState = artwork?.let { - UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it)) - } ?: UserWalletItemUM.ImageState.Loading, + imageState = getImageState(userWallet = value), + label = getLabelOrNull(userWallet = this), ) } } + private fun isEnabled(userWallet: UserWallet): Boolean { + return authMode || userWallet.isLocked.not() + } + + private fun getLabelOrNull(userWallet: UserWallet): LabelUM? { + return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + } else { + null + } + } + + private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState { + return when { + userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet + artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork)) + else -> UserWalletItemUM.ImageState.Loading + } + } + private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { val text = when (userWallet) { is UserWallet.Cold -> { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index 377ce963f1..c0abb2b485 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -1,6 +1,7 @@ package com.tangem.common.ui.userwallet.state import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId import javax.annotation.concurrent.Immutable @@ -15,6 +16,7 @@ data class UserWalletItemUM( val isEnabled: Boolean, val endIcon: EndIcon = EndIcon.None, val onClick: () -> Unit, + val label: LabelUM? = null, ) { enum class EndIcon { None, @@ -54,6 +56,8 @@ data class UserWalletItemUM( data object Loading : ImageState() + data object MobileWallet : ImageState() + data class Image( val artwork: ArtworkUM, ) : ImageState() diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 08021ec348..d04509676c 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -125,6 +125,12 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, ) : TxSentFrom("NFT"), TxData + + data class SendWithSwap( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("Send&Swap"), TxData } sealed interface TxData { @@ -229,5 +235,10 @@ sealed class AnalyticsParam { const val STANDARD = "Standard" const val NO_COLLECTION = "No collection" const val EMULATION_STATUS = "Emulation Status" + const val SEND_TOKEN = "Send Token" + const val RECEIVE_TOKEN = "Receive Token" + const val SEND_BLOCKCHAIN = "Send Blockchain" + const val RECEIVE_BLOCKCHAIN = "Receive Blockchain" + const val CHOSEN_TOKEN = "Token Chosen" } } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6ecb2f5ac6..0d8eea380b 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -50,5 +50,13 @@ { "name": "HOT_WALLET_ENABLED", "version": "undefined" + }, + { + "name": "NFT_SEND_REDESIGN_ENABLED", + "version": "undefined" + }, + { + "name": "TANGEM_PAY_ENABLED", + "version": "undefined" } ] diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index 8346378b52..fa5c04f6a1 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -14,6 +14,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.runBlocking import javax.inject.Singleton @Module @@ -41,6 +42,12 @@ internal object FeatureTogglesManagerModule { localTogglesStorage = localTogglesStorage, versionProvider = versionProvider, ) + }.also { + // We need to initialize during the hilt graph creation + // in order to provide the feature toggles correctly to other dependencies. + runBlocking { + it.init() + } } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 7aca4511ea..ce0d035439 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -9,7 +9,6 @@ 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.datasource.local.preferences.utils.storeObject -import kotlin.properties.Delegates /** * Feature toggles manager implementation in DEV build @@ -24,10 +23,14 @@ internal class DevFeatureTogglesManager( private val versionProvider: VersionProvider, ) : MutableFeatureTogglesManager { - private var featureTogglesMap: MutableMap by Delegates.notNull() - private var localFeatureTogglesMap: Map by Delegates.notNull() + private var featureTogglesMap: MutableMap? = null + private var localFeatureTogglesMap: Map? = null override suspend fun init() { + if (featureTogglesMap != null && localFeatureTogglesMap != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( @@ -46,21 +49,21 @@ internal class DevFeatureTogglesManager( .toMutableMap() } - override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap - override fun getFeatureToggles(): Map = featureTogglesMap + override fun getFeatureToggles(): Map = featureTogglesMap!! override suspend fun changeToggle(name: String, isEnabled: Boolean) { - featureTogglesMap[name] ?: return - featureTogglesMap[name] = isEnabled - appPreferencesStore.storeFeatureToggles(value = featureTogglesMap) + featureTogglesMap!![name] ?: return + featureTogglesMap!![name] = isEnabled + appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!) } override suspend fun recoverLocalConfig() { - featureTogglesMap = localFeatureTogglesMap.toMutableMap() - appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap) + featureTogglesMap = localFeatureTogglesMap!!.toMutableMap() + appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index d723297a45..54fe7a011d 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -5,7 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.storage.TogglesStorage import com.tangem.core.configtoggle.utils.associateToggles import com.tangem.core.configtoggle.version.VersionProvider -import kotlin.properties.Delegates /** * Feature toggles manager implementation in PROD build @@ -18,18 +17,22 @@ internal class ProdFeatureTogglesManager( private val versionProvider: VersionProvider, ) : FeatureTogglesManager { - private var featureToggles: Map by Delegates.notNull() + private var featureToggles: Map? = null override suspend fun init() { + if (featureToggles != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) featureToggles = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get() ?: "") } - override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun getProdFeatureToggles() = featureToggles + fun getProdFeatureToggles() = featureToggles!! @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun setProdFeatureToggles(map: Map) { diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 7c44fe9963..323153c119 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { /** Coroutines */ implementation(deps.kotlin.coroutines) implementation(deps.kotlin.coroutines.rx2) + implementation(deps.kotlin.datetime) /** Logging */ implementation(deps.timber) @@ -76,7 +77,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index 7945fc59eb..da1b1aca5f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -8,12 +8,12 @@ interface AuthProvider { /** * Returns authToken for tangem tech api */ - fun getCardPublicKey(): String + suspend fun getCardPublicKey(): String - fun getCardId(): String + suspend fun getCardId(): String /** * Returns map where keys(cardId) associated with cardPublicKey */ - fun getCardsPublicKeys(): Map + suspend fun getCardsPublicKeys(): Map } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt index 1b2a53ec3c..ee59740bff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt @@ -15,12 +15,14 @@ import okio.IOException * Switch api environment [Interceptor] * * @property id api config id [ApiConfig.ID] + * @property baseUrls base urls for all api config environments * @property apiConfigsManager api configs manager * [REDACTED_AUTHOR] */ internal class SwitchEnvironmentInterceptor( private val id: ApiConfig.ID, + private val baseUrls: Set, private val apiConfigsManager: ApiConfigsManager, ) : Interceptor { @@ -39,10 +41,13 @@ internal class SwitchEnvironmentInterceptor( return chain.proceed(request) } - private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl { - return this.newBuilder() - .host(host = url.toHttpUrl().host) - .build() + private fun HttpUrl.adjustBaseUrl(newBaseUrl: String): HttpUrl { + val currentUrl = this.toString() + val currentBaseUrl = baseUrls.first { currentUrl.contains(it) } + + return currentUrl + .replace(oldValue = currentBaseUrl, newValue = newBaseUrl) + .toHttpUrl() } private fun Request.Builder.addHeaders(headers: Map>): Request.Builder { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt index 0efb5068d1..8f0331ca8b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.api.common.config +import com.tangem.datasource.BuildConfig import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.ProviderSuspend @@ -14,20 +15,44 @@ internal class StakeKit( private val stakeKitAuthProvider: StakeKitAuthProvider, ) : ApiConfig() { - override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() override val environmentConfigs: List = listOf( createProdEnvironment(), + createMockEnvironment(), ) + private fun getInitialEnvironment(): ApiEnvironment { + return when (BuildConfig.BUILD_TYPE) { + MOCKED_BUILD_TYPE, + -> ApiEnvironment.MOCK + DEBUG_BUILD_TYPE, + INTERNAL_BUILD_TYPE, + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + } + private fun createProdEnvironment(): ApiEnvironmentConfig { return ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://api.stakek.it/v1/", - headers = mapOf( - "X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey), - "accept" to ProviderSuspend { "application/json" }, - ), + headers = createHeaders(), ) } + + private fun createMockEnvironment(): ApiEnvironmentConfig { + return ApiEnvironmentConfig( + environment = ApiEnvironment.MOCK, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(), + ) + } + + private fun createHeaders() = buildMap { + put(key = "X-API-KEY", value = ProviderSuspend(stakeKitAuthProvider::getApiKey)) + put(key = "accept", value = ProviderSuspend { "application/json" }) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 2749826753..8678e1959c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -1,19 +1,7 @@ package com.tangem.datasource.api.pay import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest -import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest -import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest -import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest -import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest -import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest -import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest -import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest -import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.pay.models.request.* import com.tangem.datasource.api.pay.models.response.* import retrofit2.http.Body import retrofit2.http.GET @@ -33,9 +21,17 @@ interface TangemPayApi { @Body request: GenerateNoneByCardWalletRequest, ): ApiResponse + @POST("v1/auth/challenge") + suspend fun generateNonceByCustomerWallet( + @Body request: GenerateNonceByCustomerWalletRequest, + ): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse + @POST("v1/auth/token") + suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt new file mode 100644 index 0000000000..edd8260545 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GenerateNonceByCustomerWalletRequest( + @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "customer_wallet_address") val customerWalletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt new file mode 100644 index 0000000000..9a5e47e327 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetTokenByCustomerWalletRequest( + @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "session_id") val sessionId: String, + @Json(name = "signature") val signature: String, + @Json(name = "message_format") val messageFormat: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index efd6c85900..c7ab58a7fd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType @JsonClass(generateAdapter = true) data class PendingActionRequestBody( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 14ec9f906f..51fd1ce19a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -1,9 +1,11 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.promotion.models.PromotionInfoResponse import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.local.config.providers.models.ProviderModel import retrofit2.http.* @@ -30,9 +32,6 @@ interface TangemTechApi { @Query("limit") limit: Int? = null, ): ApiResponse - @GET("v1/rates") - suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse - @GET("v1/currencies") suspend fun getCurrencyList( @Header("Cache-Control") cacheControl: String = "max-age=600", @@ -68,46 +67,11 @@ interface TangemTechApi { @Query("fields") fields: String, ): ApiResponse - @GET("v1/promotion") - suspend fun getPromotionInfo( - @Query("programName") name: String, - @Header("Cache-Control") cacheControl: String = "max-age=600", - ): ApiResponse - - @GET("v1/settings/{wallet_id}") - suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse - - @PUT("v1/settings/{wallet_id}") - suspend fun saveUserTokensSettings( - @Path("wallet_id") walletId: String, - @Body userTokensSettings: UserTokensSettingsResponse, - ): ApiResponse - @POST("v1/user-network-account") suspend fun createUserNetworkAccount( @Body body: CreateUserNetworkAccountBody, ): ApiResponse - @POST("v1/account") - suspend fun createUserTokensAccount( - @Body body: CreateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}") - suspend fun updateUserTokensAccount( - @Path("account_id") accountId: Int, - @Body body: UpdateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}/archive") - suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @PUT("v1/account/{account_id}/unarchive") - suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @GET("v1/features") - suspend fun getFeatures(): ApiResponse - @ReadTimeout(duration = 5, unit = TimeUnit.SECONDS) @GET("v1/networks/providers") suspend fun getBlockchainProviders(): Map> @@ -160,7 +124,7 @@ interface TangemTechApi { suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse // endregion - // region wallets + // region user-wallets @PATCH("v1/user-wallets/wallets/{wallet_id}") suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse @@ -176,4 +140,20 @@ interface TangemTechApi { @GET("v1/user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion + + // region account + @GET("/v1/wallets/{walletId}/accounts") + suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse + + @PUT("/v1/wallets/{walletId}/accounts") + suspend fun saveWalletAccounts( + @Path("walletId") walletId: String, + @Header("If-Match") ifMatch: String, + ): ApiResponse + + @GET("/v1/wallets/{walletId}/accounts/archived") + suspend fun getWalletArchivedAccounts( + @Path("walletId") walletId: String, + ): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt deleted file mode 100644 index 164cd09362..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.datasource.api.tangemTech - -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.tangemTech.models.v2.UserTokensResponseV2 -import retrofit2.http.* - -interface TangemTechApiV2 { - - @GET("user-tokens/{wallet_id}") - suspend fun getUserTokens(@Path("wallet_id") walletId: String): ApiResponse - - @PUT("user-tokens/{wallet_id}") - suspend fun saveUserTokens( - @Path("wallet_id") walletId: String, - @Body userTokens: UserTokensResponseV2, - ): ApiResponse -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 2476bf0a31..8e1dec299f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -16,6 +16,7 @@ data class UserTokensResponse( @JsonClass(generateAdapter = true) data class Token( @Json(name = "id") val id: String? = null, + @Json(name = "accountId") val accountId: String? = null, @Json(name = "networkId") val networkId: String, @Json(name = "derivationPath") val derivationPath: String? = null, @Json(name = "name") val name: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt new file mode 100644 index 0000000000..6c3afd812f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType + +@JsonClass(generateAdapter = true) +data class GetWalletAccountsResponse( + @Json(name = "wallet") val wallet: Wallet, + @Json(name = "accounts") val accounts: List, + @Json(name = "unassignedTokens") val unassignedTokens: List, +) { + + @JsonClass(generateAdapter = true) + data class Wallet( + @Json(name = "version") val version: Int, + @Json(name = "group") val group: GroupType, + @Json(name = "sort") val sort: SortType, + @Json(name = "totalAccounts") val totalAccounts: Int, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt new file mode 100644 index 0000000000..3f1db4c76b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetWalletArchivedAccountsResponse( + @Json(name = "archivedAccounts") val accounts: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt new file mode 100644 index 0000000000..3f36276519 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SaveWalletAccountsResponse( + @Json(name = "accounts") val accounts: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt new file mode 100644 index 0000000000..343b6cdf2a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +@JsonClass(generateAdapter = true) +data class WalletAccountDTO( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "derivation") val derivationIndex: Int, + @Json(name = "icon") val icon: String, + @Json(name = "iconColor") val iconColor: String, + @Json(name = "tokens") val tokens: List? = null, + @Json(name = "totalTokens") val totalTokens: Int? = null, + @Json(name = "totalNetworks") val totalNetworks: Int? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt deleted file mode 100644 index e03b095d4e..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.datasource.api.tangemTech.models.v2 - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse - -@JsonClass(generateAdapter = true) -data class UserTokensResponseV2( - @Json(name = "accounts") - val accounts: List, -) { - - @JsonClass(generateAdapter = true) - data class TokensAccount( - @Json(name = "id") - val id: Int, - @Json(name = "title") - val title: String, - @Json(name = "tokens") - val tokens: List? = null, - @Json(name = "tokensCount") - val tokensCount: Int? = null, - @Json(name = "archived") - val isArchived: Boolean, - ) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 2363c048e7..6c8dc4b626 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -1,8 +1,5 @@ package com.tangem.datasource.di -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.config.ApiConfig @@ -12,44 +9,29 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager -import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.TangemTechApiV2 -import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.datasource.di.utils.RetrofitApiBuilder +import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.utils.* -import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.info.AppInfoProvider -import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory -import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) internal object NetworkModule { - private const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L - private val excludedApiForLogging: Set = setOf( - ApiConfig.ID.StakeKit, - ) - @Provides @Singleton fun provideApiConfigManager( @@ -66,286 +48,76 @@ internal object NetworkModule { @Provides @Singleton - fun provideExpressApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): TangemExpressApi { - return createApi( - id = ApiConfig.ID.Express, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, + fun provideExpressApi(retrofitApiBuilder: RetrofitApiBuilder): TangemExpressApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.Express, + applyTimeoutAnnotations = false, ) } @Provides @Singleton - fun provideStakeKitApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - apiConfigsManager: ApiConfigsManager, - analyticsErrorHandler: AnalyticsErrorHandler, - appLogsStore: AppLogsStore, - ): StakeKitApi { - return createApi( - id = ApiConfig.ID.StakeKit, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, + fun provideStakeKitApi(retrofitApiBuilder: RetrofitApiBuilder): StakeKitApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.StakeKit, + applyTimeoutAnnotations = false, timeouts = Timeouts( callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, readTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, writeTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, ), - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, ) } @Provides @Singleton - fun provideOnrampApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): OnrampApi { - return createApi( - id = ApiConfig.ID.Express, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, + fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.Express, + applyTimeoutAnnotations = false, ) } @Provides @Singleton - fun provideTangemTechApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - ): TangemTechApi { - return createApi( - id = ApiConfig.ID.TangemTech, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { applyTimeoutAnnotations() }, - ) - } - - // TODO: It will be deleted in the future or refactored using ApiConfig - @Provides - @Singleton - fun provideTangemTechApiV2( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - appVersionProvider: AppVersionProvider, - appInfoProvider: AppInfoProvider, - ): TangemTechApiV2 { - return provideTangemTechApiInternal( - moshi = moshi, - context = context, - appVersionProvider = appVersionProvider, - baseUrl = PROD_V2_TANGEM_TECH_BASE_URL, - analyticsErrorHandler = analyticsErrorHandler, - appInfoProvider = appInfoProvider, + fun provideTangemTechApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemTech, + applyTimeoutAnnotations = true, ) } @Provides @Singleton - fun provideTangemTechMarketsApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - ): TangemTechMarketsApi { - return createApi( - id = ApiConfig.ID.TangemTech, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .readTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .applyTimeoutAnnotations() - }, + fun provideTangemTechMarketsApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechMarketsApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemTech, + applyTimeoutAnnotations = false, + timeouts = Timeouts( + callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + connectTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + readTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + ), + logsSaving = false, ) } @Provides @Singleton - fun provideTangemVisaApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): TangemPayApi { - return createApi( - id = ApiConfig.ID.TangemPay, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ).applyTimeoutAnnotations() - }, + fun provideTangemVisaApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemPay, + applyTimeoutAnnotations = false, ) } - @Suppress("LongParameterList") - @Deprecated("use createApi instead") - private inline fun provideTangemTechApiInternal( - moshi: Moshi, - context: Context, - appVersionProvider: AppVersionProvider, - appInfoProvider: AppInfoProvider, - baseUrl: String, - analyticsErrorHandler: AnalyticsErrorHandler, - timeouts: Timeouts = Timeouts(), - requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider, appInfoProvider)), - ): T { - val client = OkHttpClient.Builder() - .applyTimeoutAnnotations() - .let { builder -> - var b = builder - if (timeouts.callTimeoutSeconds != null) { - b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.connectTimeoutSeconds != null) { - b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.readTimeoutSeconds != null) { - b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.writeTimeoutSeconds != null) { - b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) - } - b - } - .addHeaders( - *requestHeaders.toTypedArray(), - // TODO("refactor header init") get auth data after biometric auth to avoid race condition - // AuthenticationHeader(authProvider), - ) - .addLoggers(context) - .build() - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) - .baseUrl(baseUrl) - .client(client) - .build() - .create(T::class.java) - } - @Provides @Singleton - fun provideBlockAidApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): BlockAidApi { - return createApi( - id = ApiConfig.ID.BlockAid, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ).applyTimeoutAnnotations() - }, + fun provideBlockAidApi(retrofitApiBuilder: RetrofitApiBuilder): BlockAidApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.BlockAid, + applyTimeoutAnnotations = false, ) } - - private inline fun createApi( - id: ApiConfig.ID, - moshi: Moshi, - context: Context, - apiConfigsManager: ApiConfigsManager, - analyticsErrorHandler: AnalyticsErrorHandler, - timeouts: Timeouts = Timeouts(), - clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this }, - ): T { - val environmentConfig = apiConfigsManager.getEnvironmentConfig(id) - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) - .baseUrl(environmentConfig.baseUrl) - .client( - OkHttpClient.Builder() - .applyApiConfig(id, apiConfigsManager) - .applyTimeoutAnnotations() - .let { builder -> - var b = builder - if (timeouts.callTimeoutSeconds != null) { - b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.connectTimeoutSeconds != null) { - b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.readTimeoutSeconds != null) { - b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.writeTimeoutSeconds != null) { - b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) - } - b - } - .addLoggers(context = context, id = id) - .clientBuilder() - .build(), - ) - .build() - .create(T::class.java) - } - - private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder { - if (id in excludedApiForLogging) return this - - return addLoggers(context) - } - - private data class Timeouts( - val callTimeoutSeconds: Long? = null, - val connectTimeoutSeconds: Long? = null, - val readTimeoutSeconds: Long? = null, - val writeTimeoutSeconds: Long? = null, - ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt new file mode 100644 index 0000000000..682bf87475 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt @@ -0,0 +1,34 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.swap.DefaultSwapBestRateAnimationStore +import com.tangem.datasource.local.swap.DefaultSwapTransactionStatusStore +import com.tangem.datasource.local.swap.SwapBestRateAnimationStore +import com.tangem.datasource.local.swap.SwapTransactionStatusStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SwapStoreModule { + + @Provides + @Singleton + fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore { + return DefaultSwapTransactionStatusStore( + dataStore = RuntimeDataStore(), + ) + } + + @Provides + @Singleton + fun provideSwapBestRateAnimationStore(): SwapBestRateAnimationStore { + return DefaultSwapBestRateAnimationStore( + dataStore = RuntimeSharedStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt deleted file mode 100644 index 25f71c60a0..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore -import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object SwapTransactionStatusStoreModule { - - @Provides - @Singleton - fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore { - return DefaultSwapTransactionStatusStore( - dataStore = RuntimeDataStore(), - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt new file mode 100644 index 0000000000..aa2adc0817 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -0,0 +1,201 @@ +package com.tangem.datasource.di.utils + +import android.content.Context +import com.chuckerteam.chucker.api.ChuckerInterceptor +import com.squareup.moshi.Moshi +import com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiConfigs +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor +import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory +import com.tangem.datasource.api.utils.ConnectTimeout +import com.tangem.datasource.api.utils.ReadTimeout +import com.tangem.datasource.api.utils.WriteTimeout +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.datasource.utils.NetworkLogsSaveInterceptor +import com.tangem.datasource.utils.addHeaders +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Invocation +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A builder class for creating Retrofit API instances + * + * @property apiConfigsManager manages API configurations for different environments + * @property moshi moshi + * @property analyticsErrorHandler handles analytics-related errors + * @property context application context + * @property appLogsStore application logs store + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class RetrofitApiBuilder @Inject constructor( + private val apiConfigs: ApiConfigs, + private val apiConfigsManager: ApiConfigsManager, + @NetworkMoshi private val moshi: Moshi, + private val analyticsErrorHandler: AnalyticsErrorHandler, + @ApplicationContext private val context: Context, + private val appLogsStore: AppLogsStore, +) { + + private val configsBaseUrls: Map> = getConfigsBaseUrls() + + /** + * Builds a Retrofit API instance for the specified API configuration ID + * + * @param apiConfigId the ID of the API configuration to use + * @param applyTimeoutAnnotations whether to apply timeout annotations to the requests. See [ReadTimeout], etc. + * @param timeouts optional timeouts for the requests + * @param logsSaving whether to enable logs saving + * + * @return an instance [T] of the specified API interface + */ + inline fun build( + apiConfigId: ApiConfig.ID, + applyTimeoutAnnotations: Boolean, + timeouts: Timeouts? = null, + logsSaving: Boolean = true, + ): T { + val environmentConfig = apiConfigsManager.getEnvironmentConfig(apiConfigId) + + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) + .baseUrl(environmentConfig.baseUrl) + .client( + OkHttpClient.Builder() + .applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig) + .let { + if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it + } + .applyTimeouts(timeouts = timeouts) + .let { + if (logsSaving) it.applyLogsSaving() else it + } + .addLoggers(apiConfigId = apiConfigId, context = context) + .build(), + ) + .build() + .create(T::class.java) + } + + data class Timeouts( + val callTimeoutSeconds: Long? = null, + val connectTimeoutSeconds: Long? = null, + val readTimeoutSeconds: Long? = null, + val writeTimeoutSeconds: Long? = null, + ) + + private fun getConfigsBaseUrls(): Map> { + return apiConfigs.associate { config -> + val allBaseUrls = config.environmentConfigs.mapTo(hashSetOf(), ApiEnvironmentConfig::baseUrl) + + config.id to allBaseUrls + } + } + + private fun OkHttpClient.Builder.applyApiConfig( + apiConfigId: ApiConfig.ID, + environmentConfig: ApiEnvironmentConfig, + ): OkHttpClient.Builder { + return if (BuildConfig.TESTER_MENU_ENABLED) { + addInterceptor( + interceptor = SwitchEnvironmentInterceptor( + id = apiConfigId, + baseUrls = configsBaseUrls[apiConfigId] + ?: error("Base URLs for ApiConfig with id [$apiConfigId] not found"), + apiConfigsManager = apiConfigsManager, + ), + ) + } else { + val headers = environmentConfig.headers + + this.addHeaders(headers) + } + } + + private fun OkHttpClient.Builder.applyTimeouts(timeouts: Timeouts?): OkHttpClient.Builder { + if (timeouts == null) return this + + var b = this + + if (timeouts.callTimeoutSeconds != null) { + b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.connectTimeoutSeconds != null) { + b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.readTimeoutSeconds != null) { + b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.writeTimeoutSeconds != null) { + b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) + } + + return b + } + + /** + * Apply timeout annotations [Interceptor]. + * Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests. + */ + private fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder { + return addInterceptor( + Interceptor { chain -> + val request = chain.request() + val tag = request.tag(Invocation::class.java) + val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java) + val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) + val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) + + chain + .run { + connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this + } + .run { + readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this + } + .run { + writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this + } + .proceed(request) + }, + ) + } + + private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder { + return addInterceptor( + interceptor = NetworkLogsSaveInterceptor(appLogsStore), + ) + } + + private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder { + if (apiConfigId in excludedApiForLogging) return this + + return if (BuildConfig.LOG_ENABLED) { + addInterceptor(interceptor = ChuckerInterceptor(context)) + addInterceptor(interceptor = createNetworkLoggingInterceptor()) + } else { + this + } + } + + private companion object { + + val excludedApiForLogging: Set = setOf( + // ApiConfig.ID.StakeKit, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index d1b906f30e..06a5f51102 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -84,6 +84,10 @@ object PreferencesKeys { val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") } + val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") } + + val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") } + val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy { @@ -149,6 +153,8 @@ object PreferencesKeys { val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } + + fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") // endregion // region Promo @@ -163,6 +169,18 @@ object PreferencesKeys { fun getShouldShowInitialPermissionScreen(permission: String) = booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") // endregion + + // region Hot Wallet unlock attempts + + fun getHotWalletUnlockAttemptsKey(attemptId: String) = + intPreferencesKey(name = "hotWalletUnlockAttempts_$attemptId") + + fun getHotWalletUnlockBootKey(attemptId: String) = intPreferencesKey(name = "hotWalletUnlockBootCount_$attemptId") + + fun getHotWalletUnlockDeadlineKey(attemptId: String) = + longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId") + + // endregion } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt new file mode 100644 index 0000000000..4f207731c1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.swap + +import com.tangem.datasource.local.datastore.RuntimeSharedStore + +internal class DefaultSwapBestRateAnimationStore( + private val dataStore: RuntimeSharedStore, +) : SwapBestRateAnimationStore, RuntimeSharedStore by dataStore { + /** + * Returns flag indicating whether should show best rate animation in current session. + * Animation should appear once per session + * + * If true, reset flag to false + */ + override suspend fun getSyncOrNull(): Boolean { + val value = dataStore.getSyncOrNull() ?: true + if (value) { + dataStore.store(false) + } + return value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapTransactionStatusStore.kt similarity index 91% rename from core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapTransactionStatusStore.kt index bb094d028f..87881b3768 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapTransactionStatusStore.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.swaptx +package com.tangem.datasource.local.swap import com.tangem.datasource.local.datastore.core.StringKeyDataStore diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt new file mode 100644 index 0000000000..7fc8e8a1d0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.swap + +/** + * Stores flag indicating whether should show best rate animation in current session. + * Animation should appear once per session + * + * If true, reset flag to false + */ +interface SwapBestRateAnimationStore { + suspend fun getSyncOrNull(): Boolean +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapTransactionStatusStore.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapTransactionStatusStore.kt index 033f71b6ac..af554bba1a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapTransactionStatusStore.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.swaptx +package com.tangem.datasource.local.swap /** * Runtime cache for storing swap transactions statuses sent to analytics diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt index a1e9edcb49..772c349e0a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO -import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.models.staking.BalanceType import com.tangem.utils.converter.Converter internal object BalanceTypeConverter : Converter { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt index 2284023080..129ecf3eda 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.PendingActionConstraints +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.PendingActionConstraints import com.tangem.utils.converter.Converter internal object PendingActionConstraintsConverter : diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt index 716d5fc960..161bc9885c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.utils.converter.Converter internal object PendingActionConverter : Converter { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt index d95777148f..f5300b3327 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.utils.converter.Converter @Suppress("CyclomaticComplexMethod") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt index e69821a778..adc411b5e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType import com.tangem.utils.converter.TwoWayConverter @Suppress("CyclomaticComplexMethod", "LongMethod") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index de55373f39..828449590f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -1,53 +1,61 @@ package com.tangem.datasource.local.token.converter +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceItem +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.utils.converter.Converter +import kotlinx.datetime.Instant class YieldBalanceConverter( private val source: StatusSource, -) : Converter { +) : Converter { constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL) - override fun convert(value: YieldBalanceWrapperDTO): YieldBalance { + override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? { + val stakingId = StakingID( + integrationId = value.integrationId ?: return null, + address = value.addresses.address, + ) + return if (value.balances.isEmpty()) { - YieldBalance.Empty( - integrationId = value.integrationId, - address = value.addresses.address, - source = source, - ) + YieldBalance.Empty(stakingId = stakingId, source = source) } else { YieldBalance.Data( - integrationId = value.integrationId, - address = value.addresses.address, + stakingId = stakingId, balance = YieldBalanceItem( - items = value.balances.map { item -> - BalanceItem( - groupId = item.groupId, - token = TokenConverter.convert(item.tokenDTO), - type = BalanceTypeConverter.convert(item.type), - amount = item.amount, - rawCurrencyId = item.tokenDTO.coinGeckoId, - // tron-specific. operates validatorAddresses instead of validatorAddress - validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0), - date = item.date?.toDateTime(), - pendingActions = PendingActionConverter - .convertList(item.pendingActions) - .sortedBy { it.passthrough }, - pendingActionsConstraints = PendingActionConstraintsConverter - .convertList(item.pendingActionConstraints.orEmpty()), - isPending = false, - ) - } - .sortedWith(compareBy({ it.type }, { it.amount })), + items = value.balances + .map { item -> item.toBalanceItem() } + .sortedWith(comparator = compareBy({ it.type }, { it.amount })), integrationId = value.integrationId, ), source = source, ) } } + + private fun BalanceDTO.toBalanceItem(): BalanceItem { + val item = this + + return BalanceItem( + groupId = item.groupId, + token = YieldTokenConverter.convert(item.tokenDTO), + type = BalanceTypeConverter.convert(item.type), + amount = item.amount, + rawCurrencyId = item.tokenDTO.coinGeckoId, + // tron-specific. operates validatorAddresses instead of validatorAddress + validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0), + date = item.date?.toString()?.let { Instant.parse(it) }, + pendingActions = PendingActionConverter + .convertList(item.pendingActions) + .sortedBy { it.passthrough }, + pendingActionsConstraints = PendingActionConstraintsConverter + .convertList(item.pendingActionConstraints.orEmpty()), + isPending = false, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldTokenConverter.kt similarity index 77% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldTokenConverter.kt index 4a363aa324..4c4e1934df 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldTokenConverter.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.models.staking.YieldToken import com.tangem.utils.converter.TwoWayConverter -object TokenConverter : TwoWayConverter { +object YieldTokenConverter : TwoWayConverter { - override fun convert(value: TokenDTO): Token { - return Token( + override fun convert(value: TokenDTO): YieldToken { + return YieldToken( name = value.name, network = StakingNetworkTypeConverter.convert(value.network), symbol = value.symbol, @@ -19,7 +19,7 @@ object TokenConverter : TwoWayConverter { ) } - override fun convertBack(value: Token): TokenDTO { + override fun convertBack(value: YieldToken): TokenDTO { return TokenDTO( name = value.name, network = StakingNetworkTypeConverter.convertBack(value.network), diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 4f1d6b5cba..b4d50f01de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -5,6 +5,10 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow +@Deprecated( + message = "Use UserWalletsListRepository instead", + replaceWith = ReplaceWith("UserWalletsListRepository"), +) interface UserWalletsStore { val selectedUserWalletOrNull: UserWallet? @@ -15,8 +19,6 @@ interface UserWalletsStore { fun getSyncStrict(key: UserWalletId): UserWallet - suspend fun getAllSyncOrNull(): List? - suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index ed7d4c62b3..eb040fb1e4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -1,21 +1,9 @@ package com.tangem.datasource.utils -import android.content.Context -import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.tangem.datasource.BuildConfig -import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor -import com.tangem.datasource.api.common.config.ApiConfig -import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE -import com.tangem.datasource.api.common.config.managers.ApiConfigsManager -import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.datasource.api.utils.ConnectTimeout -import com.tangem.datasource.api.utils.ReadTimeout -import com.tangem.datasource.api.utils.WriteTimeout import com.tangem.utils.ProviderSuspend import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.OkHttpClient -import retrofit2.Invocation /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder { @@ -24,30 +12,6 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade ) } -/** - * Apply timeout annotations [Interceptor]. - * Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests. - */ -internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder { - return addInterceptor( - Interceptor { chain -> - val request = chain.request() - val tag = request.tag(Invocation::class.java) - val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java) - val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) - val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) - - chain.run { - connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this - }.run { - readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this - }.run { - writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this - }.proceed(request) - }, - ) -} - /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders( requestHeaders: Map>, @@ -66,44 +30,4 @@ internal fun OkHttpClient.Builder.addHeaders( chain.proceed(request) }, ) -} - -/** - * Extension for logging each [OkHttpClient] request - * - * @param context context - */ -internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder { - return if (BuildConfig.LOG_ENABLED) { - context?.let { - addInterceptor(interceptor = ChuckerInterceptor(it)) - } - addInterceptor(interceptor = createNetworkLoggingInterceptor()) - } else { - this - } -} - -/** - * Apply api config - * - * @param id class of [ApiConfig] - * @param apiConfigsManager api configs manager - */ -internal fun OkHttpClient.Builder.applyApiConfig( - id: ApiConfig.ID, - apiConfigsManager: ApiConfigsManager, -): OkHttpClient.Builder { - return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { - addInterceptor( - interceptor = SwitchEnvironmentInterceptor( - id = id, - apiConfigsManager = apiConfigsManager, - ), - ) - } else { - val headers = apiConfigsManager.getEnvironmentConfig(id).headers - - this.addHeaders(headers) - } } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index defd5db207..61d25e29b1 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -18,6 +18,7 @@ import com.tangem.utils.ProviderSuspend import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.version.AppVersionProvider import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -55,8 +56,8 @@ internal class ProdApiConfigsManagerTest { every { appVersionProvider.versionName } returns VERSION_NAME every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY - every { appAuthProvider.getCardId() } returns APP_CARD_ID - every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY + coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID + coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY every { appInfoProvider.osVersion } returns "Android 16" } diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 5ffb365ef3..2e061c0bce 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -478,6 +478,8 @@ å®Ÿč”Œć™ć‚‹ćØć€ęœ€åˆć‹ć‚‰ć‚„ć‚Šē›“ć™åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚ Googlećƒ‰ćƒ©ć‚¤ćƒ–ć®ćƒćƒƒć‚Æć‚¢ćƒƒćƒ—ć«äæå­˜ć•ć‚Œć¦ć„ć‚‹ę—¢å­˜ć®ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć‚’å¾©å…ƒć™ć‚‹ Googlećƒ‰ćƒ©ć‚¤ćƒ–ć®ćƒćƒƒć‚Æć‚¢ćƒƒćƒ— + Tangemć®ę„­ē•Œęœ€é«˜ę°“ęŗ–ć®ćƒćƒ¼ćƒ‰ć‚¦ć‚§ć‚¢ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć§ć€ä»Šć™ćć‚»ć‚­ćƒ„ćƒŖćƒ†ć‚£ć‚’ć‚¢ćƒƒćƒ—ć‚°ćƒ¬ćƒ¼ćƒ‰ć—ć¾ć—ć‚‡ć†ć€‚ + ćƒćƒ¼ćƒ‰ć‚¦ć‚§ć‚¢ć‚¦ć‚©ćƒ¬ćƒƒćƒˆ ćƒćƒƒć‚Æć‚¢ćƒƒćƒ—ćøē§»å‹• ć‚¢ć‚Æć‚»ć‚¹ć‚³ćƒ¼ćƒ‰ć‚’ä½æē”Øć—ć¦ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć‚’äæč­·ć™ć‚‹ć«ćÆć€ć¾ćšćƒćƒƒć‚Æć‚¢ćƒƒćƒ—ć‚’å®Œäŗ†ć—ć¦ćć ć•ć„ć€‚ ć¾ćšćƒćƒƒć‚Æć‚¢ćƒƒćƒ—ć‚’å®Œäŗ†ć™ć‚‹ @@ -491,6 +493,19 @@ ćƒ¢ćƒć‚¤ćƒ«ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć‚’ä½œęˆć™ć‚‹ ć“ć®ćƒŖć‚«ćƒćƒŖćƒ¼ćƒ•ćƒ¬ćƒ¼ć‚ŗćÆć™ć§ć«ć‚¤ćƒ³ćƒćƒ¼ćƒˆć•ć‚Œć¦ć„ć¾ć™ć€‚ ćƒ¢ćƒć‚¤ćƒ«ć‚¦ć‚©ćƒ¬ćƒƒćƒˆ + ę‰‹ē¶šćäø­ć‚‚č³‡é‡‘ćÆå®‰å…Øć«äæē®”ć•ć‚Œć€å®Œå…Øć«ć‚¢ć‚Æć‚»ć‚¹åÆčƒ½ć§ć™ + 資金へのアクセス + ć™ć¹ć¦ć®ćƒ—ćƒ©ć‚¤ćƒ™ćƒ¼ćƒˆć‚¦ć‚©ćƒ¬ćƒƒćƒˆćƒ‡ćƒ¼ć‚æćÆćƒ¢ćƒć‚¤ćƒ«ć‚¢ćƒ—ćƒŖć‹ć‚‰å‰Šé™¤ć•ć‚Œć€Tangemćƒ‡ćƒć‚¤ć‚¹ć«ć®ćæå®‰å…Øć«äæå­˜ć•ć‚Œć¾ć™ć€‚ + ć‚»ć‚­ćƒ„ćƒŖćƒ†ć‚£å…Øčˆ¬ + ē§˜åÆ†éµćÆć€ć‚¢ćƒ—ćƒŖć‹ć‚‰Tangemć‚«ćƒ¼ćƒ‰ćƒ»ćƒŖćƒ³ć‚°ć«ē§»å‹•ć—ć¾ć™ + éµć®ē§»č”Œ + ćƒ‡ćƒć‚¤ć‚¹ć‚’ć‚¹ć‚­ćƒ£ćƒ³ + ć‚¢ćƒƒćƒ—ć‚°ćƒ¬ćƒ¼ćƒ‰ć‚’é–‹å§‹ + ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć‚’Tangemć‚¦ć‚©ćƒ¬ćƒƒćƒˆć«ć‚¢ćƒƒćƒ—ć‚°ćƒ¬ćƒ¼ćƒ‰ć—ć¾ć™ć€‚ć“ć‚Œć«ć‚ˆć‚Šć€ć‚³ćƒ¼ćƒ«ćƒ‰ć‚¹ćƒˆćƒ¬ćƒ¼ć‚øć§č³‡ē”£ć‚’å®‰å…Øć«äæē®”ć§ćć¾ć™ć€‚ + Tangemć‚¦ć‚©ćƒ¬ćƒƒćƒˆ + ćƒćƒ¼ćƒ‰ć‚¦ć‚§ć‚¢ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć«ć‚¢ćƒƒćƒ—ć‚°ćƒ¬ćƒ¼ćƒ‰ + Tangemć®ę„­ē•Œęœ€é«˜ę°“ęŗ–ć®ćƒćƒ¼ćƒ‰ć‚¦ć‚§ć‚¢ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć§ć€ęš—å·č³‡ē”£ć‚’å®‰å…Øć«äæē®”ć—ć¾ć—ć‚‡ć†ć€‚ + ćƒćƒ¼ćƒ‰ć‚¦ć‚§ć‚¢ćƒćƒƒć‚Æć‚¢ćƒƒćƒ—ć§ć‚¦ć‚©ćƒ¬ćƒƒćƒˆć‚’ć‚¢ćƒƒćƒ—ć‚°ćƒ¬ćƒ¼ćƒ‰ ć“ć®ęƒ…å ±ćÆAIć§ē”Ÿęˆć•ć‚Œć¾ć—ćŸć€‚ \nć‚Øćƒ©ćƒ¼ćŒč¦‹ć¤ć‹ć£ćŸå “åˆćÆć€ć“ć“ć‚’ć‚æćƒƒćƒ—ć—ć¦ćć ć•ć„ć€‚ ć‚¢ć‚Æć‚»ć‚¹ć‚³ćƒ¼ćƒ‰ć‚’å¤‰ę›“ć™ć‚‹ć«ćÆć€äøŠå›³ć®ć‚ˆć†ć«ć‚«ćƒ¼ćƒ‰ć¾ćŸćÆćƒŖćƒ³ć‚°ć‚’ć‚æćƒƒćƒ—ć—ć€ę“ä½œćŒēµ‚äŗ†ć™ć‚‹ć¾ć§å–ć‚Šå¤–ć•ćŖć„ć§ćć ć•ć„ć€‚ ćƒ‘ć‚¹ć‚³ćƒ¼ćƒ‰ć‚’å¤‰ę›“ć™ć‚‹ć«ćÆć€äøŠčØ˜ć®ć‚ˆć†ć«ć‚«ćƒ¼ćƒ‰ć‚’ć‚æćƒƒćƒ—ć—ć€ę“ä½œćŒēµ‚äŗ†ć™ć‚‹ć¾ć§å–ć‚Šå¤–ć•ćŖć„ć§ćć ć•ć„ć€‚ diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8bda8073a5..dc9ced8f5c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -485,6 +485,8 @@ If you do, you\'ll need to start over. Recover an existing wallet stored in your Google Drive backup Google Drive Backup + Upgrade your security right away with aĀ best inĀ class hardware wallet from Tangem. + Hardware Wallet Go to backup To secure your wallet with a Access Code, complete the backup first. Finish Backup First @@ -498,6 +500,19 @@ Create Mobile Wallet This recovery phrase has already been imported Mobile Wallet + Your funds stay safe and fully accessible during the process + Funds access + All private wallet data will be removed from the mobile app and stored securely on your Tangem device only + General Security + Private keys will be moved from the app to your Tangem card or ring + Key Migration + Scan device + Start upgrade + You’re about to upgrade your wallet toĀ Tangem Wallet. This will keep your assets safe with cold storage. + Tangem Wallet + Upgrade to Hardware Wallet + Keep your crypto safe with Tangem’s best-in-class hardware wallet. + Upgrade wallet with a hardware
backup This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 7c76425a6e..3f46d7909d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { /** Compose */ implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.paging) implementation(deps.compose.ui.tooling) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index bd95803a6a..4c1b0f0e4f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -32,9 +32,9 @@ fun AppBarWithBackButton( TangemTopAppBar( modifier = modifier, title = text, - startButton = TopAppBarButtonUM( + startButton = TopAppBarButtonUM.Icon( iconRes = iconRes ?: R.drawable.ic_back_24, - onIconClicked = onBackClick, + onClicked = onBackClick, ), containerColor = containerColor, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index aaa6fcf804..47544cffc6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -27,14 +27,14 @@ fun AppBarWithBackButtonAndIcon( title = text, subtitle = subtitle, containerColor = backgroundColor, - startButton = TopAppBarButtonUM( + startButton = TopAppBarButtonUM.Icon( iconRes = backIconRes ?: R.drawable.ic_back_24, - onIconClicked = onBackClick, + onClicked = onBackClick, ), endButton = if (iconRes != null && onIconClick != null) { - TopAppBarButtonUM( + TopAppBarButtonUM.Icon( iconRes = iconRes, - onIconClicked = onIconClick, + onClicked = onIconClick, ) } else { null diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt index 716d79978f..c936234a4b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -20,6 +21,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TopAppBarTestTags /** * [TangemTopAppBar] height options. @@ -127,6 +129,7 @@ fun TangemTopAppBar( TopAppBarButton( button = endButton, tint = iconTint, + modifier = Modifier.testTag(TopAppBarTestTags.MORE_BUTTON), ) } }, @@ -172,6 +175,7 @@ fun TangemTopAppBar( TopAppBarButton( button = startButton, tint = iconTint, + modifier = Modifier.testTag(TopAppBarTestTags.CLOSE_BUTTON), ) } } @@ -222,6 +226,7 @@ private fun TopAppBarTitle( color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(TopAppBarTestTags.TITLE), ) AnimatedVisibility( @@ -296,25 +301,25 @@ private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider { + IconButton( + enabled = button.enabled, + modifier = modifier.size(TangemTheme.dimens.size32), + onClick = button.onClicked, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = button.iconRes), + tint = tint, + contentDescription = null, + ) + } + } + is TopAppBarButtonUM.Text -> { + Text( + modifier = modifier + .conditional(button.enabled) { + clickable { button.onClicked() } + } + .padding(4.dp), + text = button.text.resolveReference(), + color = tint, + style = TangemTheme.typography.body1, + ) + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt index 7104e25cf9..aa06b614a0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt @@ -2,21 +2,39 @@ package com.tangem.core.ui.components.appbar.models import androidx.annotation.DrawableRes import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference -data class TopAppBarButtonUM( - @DrawableRes val iconRes: Int, - val onIconClicked: () -> Unit, - val enabled: Boolean = true, +sealed class TopAppBarButtonUM( + open val onClicked: () -> Unit, + open val enabled: Boolean = true, ) { + data class Icon( + @DrawableRes val iconRes: Int, + override val onClicked: () -> Unit, + override val enabled: Boolean = true, + ) : TopAppBarButtonUM(onClicked, enabled) + + data class Text( + val text: TextReference, + override val onClicked: () -> Unit, + override val enabled: Boolean = true, + ) : TopAppBarButtonUM(onClicked, enabled) + @Suppress("FunctionName") companion object { fun Back(onBackClicked: () -> Unit) = Back(true, onBackClicked) - fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = TopAppBarButtonUM( + fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = Icon( iconRes = R.drawable.ic_back_24, - onIconClicked = onBackClicked, + onClicked = onBackClicked, + enabled = enabled, + ) + + fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text( + text = text, + onClicked = onTextClicked, enabled = enabled, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt index d729408a97..8aab6c9598 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -38,6 +39,7 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { ) Text( + modifier = Modifier.weight(1f), text = model.text.resolveReference(), style = TangemTheme.typography.subtitle1, color = when (model.accentType) { @@ -48,6 +50,8 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { maxLines = 1, overflow = TextOverflow.Ellipsis, ) + + model.label?.let { Label(it) } } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt index df7e04af33..c950b5e4e7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.block.model import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference data class BlockUM( @@ -8,6 +9,7 @@ data class BlockUM( @DrawableRes val iconRes: Int, val onClick: () -> Unit, val accentType: AccentType = AccentType.NONE, + val label: LabelUM? = null, ) { enum class AccentType { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt index ff085bffb5..dc68d70134 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt @@ -179,6 +179,7 @@ inline fun BasicModalBottomSheet( onBack = onBack, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -190,6 +191,7 @@ inline fun BasicModalBottomSheet( contentWindowInsets = { WindowInsetsZero }, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } } @@ -200,58 +202,62 @@ inline fun BasicModalBottomSheet( @Composable private fun TangemModalBottomSheet_Preview() { TangemThemePreview { - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TangemBottomSheetConfigContentPreviewConfig(), - ), - title = { - TangemModalBottomSheetTitle( - endIconRes = R.drawable.ic_close_24, - onEndClick = {}, - ) - }, - content = { - Column( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(100)) - .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) - .padding(12.dp), - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_alert_24), - ), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, + Box( + Modifier.background(TangemTheme.colors.background.tertiary), + ) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContentPreviewConfig(), + ), + title = { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, ) - SpacerH24() - Text( - text = "Unsuported networks", - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - text = "Tangem does not currently support aĀ required network by React App.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH(48.dp) - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = "Go it", - onClick = {}, - ) - } - }, - ) + }, + content = { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(100)) + .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) + .padding(12.dp), + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_alert_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerH24() + Text( + text = "Unsuported networks", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = "Tangem does not currently support aĀ required network by React App.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH(48.dp) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Go it", + onClick = {}, + ) + } + }, + ) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt index 479fe79469..9964ac424d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.core.ui.utils.toPx /** * Modal bottom sheet with [content], [footer] and optional [title]. @@ -154,6 +155,21 @@ inline fun BasicModalBottomSheetWit val initial = 0 val scrollState = rememberScrollState(initial = initial) + val isKeyboardOpen by rememberIsKeyboardVisible() + val buttonHeight = TangemTheme.dimens.spacing80 + val contentBottomPadding = TangemTheme.dimens.spacing80 + // Offset calculation for keyboard scroll adjustment: + // 1) Button height (footer) + // 2) Column content bottom padding + // 3) Additional spacing (40dp) for visual comfort when keyboard is open + val scrollOffset = buttonHeight.toPx() + buttonHeight.toPx() + 40.dp.toPx() + + LaunchedEffect(isKeyboardOpen) { + if (isKeyboardOpen) { + scrollState.animateScrollTo(scrollState.value + scrollOffset.toInt()) + } + } + Column( modifier = Modifier .systemBarsPadding() @@ -186,7 +202,7 @@ inline fun BasicModalBottomSheetWit Column( modifier = Modifier .verticalScroll(state = scrollState) - .padding(bottom = TangemTheme.dimens.spacing80), + .padding(bottom = contentBottomPadding), ) { content(model) } @@ -199,7 +215,7 @@ inline fun BasicModalBottomSheetWit Box( modifier = Modifier .fillMaxWidth() - .height(80.dp) + .height(buttonHeight) .align(Alignment.BottomCenter), ) { footer(model) @@ -219,6 +235,7 @@ inline fun BasicModalBottomSheetWit onBack = onBack, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -230,6 +247,7 @@ inline fun BasicModalBottomSheetWit contentWindowInsets = { WindowInsetsZero }, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt index 177cd551ad..1ecce16ab7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt @@ -192,6 +192,7 @@ inline fun BasicBottomSheet( dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, onBack = onBack, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -203,6 +204,7 @@ inline fun BasicBottomSheet( contentWindowInsets = { WindowInsetsZero }, dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt index 8ca60f6218..9003e830c0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt @@ -57,9 +57,9 @@ private fun Preview_TangemBottomSheetTitle() { TangemThemePreview { TangemBottomSheetTitle( title = "Title", - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_information_24, - onIconClicked = {}, + onClicked = {}, ), containerColor = TangemTheme.colors.background.secondary, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index c7f0ba8246..8f5326a498 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -21,6 +22,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -32,7 +34,9 @@ fun HorizontalActionChips( contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0), ) { LazyRow( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS), horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), verticalAlignment = Alignment.CenterVertically, contentPadding = contentPadding, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 77f984d3c8..a7fa9386f9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -33,6 +34,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags /** * Rounded action button @@ -98,7 +100,7 @@ fun ActionButton( ), ) }, - modifier = modifier, + modifier = modifier.testTag(TokenDetailsScreenTestTags.ACTION_BUTTON), color = color, containerColor = containerColor, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 3a2dfe9d3d..aa6f681bce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -25,10 +25,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.DialogTestTags +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.utils.MultipleClickPreventer -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @Composable fun TangemButton( text: String, @@ -51,7 +51,7 @@ fun TangemButton( Button( modifier = modifier .heightIn(min = size.toHeightDp()) - .testTag(DialogTestTags.BUTTON), + .testTag(BaseButtonTestTags.BUTTON), onClick = { multipleClickPreventer.processEvent { if (!showProgress) onClick() } }, @@ -78,7 +78,8 @@ fun TangemButton( ResizableText( modifier = Modifier .weight(1f, fill = false) - .heightIn(MinButtonContentSize, maxContentSize), + .heightIn(MinButtonContentSize, maxContentSize) + .testTag(BaseButtonTestTags.TEXT), text = text, style = textStyle, color = colors.contentColor(enabled = enabled).value, @@ -92,7 +93,8 @@ fun TangemButton( Icon( modifier = Modifier .buttonContentSize(maxContentSize) - .padding(vertical = 2.dp), + .padding(vertical = 2.dp) + .testTag(BaseButtonTestTags.ICON), painter = painterResource(id = iconResId), tint = colors.contentColor(enabled = enabled).value, contentDescription = null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt index 39a042f707..c0d2dac09e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt @@ -3,10 +3,11 @@ package com.tangem.core.ui.components.buttons.small import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -41,20 +42,19 @@ fun TangemIconButton( background: Color = TangemTheme.colors.button.secondary, iconTint: Color = TangemTheme.colors.icon.secondary, ) { - IconButton( - onClick = onClick, + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), + contentDescription = "", + tint = iconTint, modifier = modifier + .size(24.dp) .clip(shape) .background(background) - .size(24.dp), - ) { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), - contentDescription = "", - tint = iconTint, - modifier = Modifier.size(16.dp), - ) - } + .padding(4.dp) + .clickable( + onClick = onClick, + ), + ) } // region Preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt index be2a3778ff..152ff9caf6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index be5c0a9f2c..2219e2642d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -12,12 +12,8 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphIntrinsics +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -28,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -77,24 +74,10 @@ fun AmountTextField( ) { val decimalFormat = rememberDecimalFormat() BoxWithConstraints(modifier = modifier) { - var fontSize = textStyle.fontSize - if (isAutoResize) { - val calculateIntrinsics = @Composable { - val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text - ParagraphIntrinsics( - text = transformedText, - style = textStyle.copy(fontSize = fontSize), - density = LocalDensity.current, - fontFamilyResolver = createFontFamilyResolver(LocalContext.current), - ) - } - var intrinsics = calculateIntrinsics() - with(LocalDensity.current) { - while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { - fontSize *= reduceFactor - intrinsics = calculateIntrinsics() - } - } + val fontSize = if (isAutoResize) { + resizeFont(visualTransformation, value, textStyle, reduceFactor) + } else { + textStyle.fontSize } val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color SimpleTextField( @@ -121,7 +104,9 @@ fun AmountTextField( singleLine = true, readOnly = !isEnabled, visualTransformation = visualTransformation, - modifier = Modifier.background(backgroundColor), + modifier = Modifier + .background(backgroundColor) + .testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt new file mode 100644 index 0000000000..23a4cf2348 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt @@ -0,0 +1,204 @@ +package com.tangem.core.ui.components.fields + +import android.annotation.SuppressLint +import androidx.annotation.FloatRange +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphIntrinsics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Simple text field for auto size input. + * Can display aligned placeholder. + * + * @param value initial text + * @param onValueChange callback + * @param isAutoResize is text font auto resize + * @param reduceFactor font resize factor + * @param textStyle text and placeholder styles + * @param textFieldModifier modifier for [SimpleTextField] + * @param boxModifier modifier for [BoxWithConstraints] + * @see [SimpleTextField] for other text field params + */ +@SuppressLint("UnusedBoxWithConstraintsScope") +@Composable +fun AutoSizeTextField( + value: String, + onValueChange: (String) -> Unit, + + // region AutoSize + isAutoResize: Boolean = true, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + reduceFactor: Double = 0.9, + + // region TextField + textFieldModifier: Modifier = Modifier, + boxModifier: Modifier = Modifier, + placeholder: TextReference? = null, + singleLine: Boolean = isAutoResize, + centered: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + color: Color = TangemTheme.colors.text.primary1, + textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), + placeholderColor: Color = TangemTheme.colors.text.disabled, + readOnly: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + isValuePasted: Boolean = false, + onValuePastedTriggerDismiss: () -> Unit = {}, + decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, +) { + BoxWithConstraints(modifier = boxModifier) { + val fontSize = if (isAutoResize) { + resizeFont(visualTransformation, value, textStyle, reduceFactor) + } else { + textStyle.fontSize + } + val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color + SimpleTextField( + value = value, + onValueChange = onValueChange, + textStyle = textStyle.copy( + fontSize = fontSize, + textDirection = TextDirection.ContentOrLtr, + ), + isValuePasted = isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + color = textColor, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + placeholder = placeholder, + placeholderColor = placeholderColor, + singleLine = singleLine, + interactionSource = interactionSource, + readOnly = readOnly, + centered = centered, + visualTransformation = visualTransformation, + decorationBox = decorationBox, + modifier = textFieldModifier, + ) + } +} + +@Composable +internal fun BoxWithConstraintsScope.resizeFont( + visualTransformation: VisualTransformation, + value: String, + textStyle: TextStyle, + reduceFactor: Double, +): TextUnit { + var result = textStyle.fontSize + val calculateIntrinsics = @Composable { + val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text + ParagraphIntrinsics( + text = transformedText, + style = textStyle.copy(fontSize = result), + density = LocalDensity.current, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + ) + } + var intrinsics = calculateIntrinsics() + with(LocalDensity.current) { + while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { + result *= reduceFactor + intrinsics = calculateIntrinsics() + } + } + return result +} + +// region preview +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun AmountTextFieldPreview( + @PreviewParameter(AutoSizeTextFieldPreviewProvider::class) data: AutoSizeTextFieldPreviewData, +) { + var text by remember { mutableStateOf(data.value) } + TangemThemePreview { + AutoSizeTextField( + textFieldModifier = Modifier.fillMaxWidth(), + value = text, + onValueChange = { text = it }, + centered = data.centered, + isAutoResize = data.isAutoResize, + placeholder = data.placeholder, + ) + } +} + +private class AutoSizeTextFieldPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextField", + placeholder = stringReference("placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "", + placeholder = stringReference("Placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + AutoSizeTextFieldPreviewData( + value = "", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + ) +} + +private data class AutoSizeTextFieldPreviewData( + val value: String, + val placeholder: TextReference, + val isAutoResize: Boolean, + val centered: Boolean, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index f46d5a579c..214da97109 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields import androidx.compose.animation.* import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -36,9 +37,9 @@ fun PinTextField( value: String, length: Int, isPasswordVisual: Boolean, + pinTextColor: PinTextColor, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, - wrongCode: Boolean = false, ) { val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { @@ -72,7 +73,7 @@ fun PinTextField( CellDecoration( length = length, isPasswordVisual = isPasswordVisual, - wrongCode = wrongCode, + pinTextColor = pinTextColor, value = value, ) }, @@ -84,17 +85,25 @@ fun PinTextField( } } -@Suppress("MagicNumber") +enum class PinTextColor { + Primary, + WrongCode, + Success, +} + +@Suppress("MagicNumber", "LongMethod") @Composable private fun CellDecoration( length: Int, - wrongCode: Boolean, + pinTextColor: PinTextColor, value: String, modifier: Modifier = Modifier, isPasswordVisual: Boolean = false, ) { val textMeasurer = rememberTextMeasurer() - val width = textMeasurer.measure("0") + val minSize = textMeasurer.measure("0") + val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell + val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell Row( modifier = modifier, @@ -107,6 +116,18 @@ private fun CellDecoration( "" } + val color = when (pinTextColor) { + PinTextColor.Primary -> { + if (isPasswordVisual) { + TangemTheme.colors.icon.informative + } else { + TangemTheme.colors.text.primary1 + } + } + PinTextColor.WrongCode -> TangemTheme.colors.icon.warning + PinTextColor.Success -> TangemTheme.colors.icon.accent + } + Box( modifier = Modifier .background( @@ -119,26 +140,34 @@ private fun CellDecoration( targetState = char, transitionSpec = { ( - fadeIn(animationSpec = tween(220, delayMillis = 90)) + - slideInVertically(animationSpec = tween(330, delayMillis = 0)) + fadeIn(animationSpec = tween(90, delayMillis = 90)) + + slideInVertically(animationSpec = tween(220, delayMillis = 0)) ) .togetherWith( fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)), ) }, ) { text -> - Text( - modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), - text = text, - style = TangemTheme.typography.h3, - color = if (wrongCode) { - TangemTheme.colors.text.warning - } else { - TangemTheme.colors.text.primary1 - }, - textAlign = TextAlign.Center, - lineHeight = 48.sp, - ) + if (isPasswordVisual && text.isNotEmpty()) { + Canvas( + Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + ) { + drawCircle( + color = color, + radius = 4.dp.toPx(), + center = center, + ) + } + } else { + Text( + modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + text = text, + style = TangemTheme.typography.h3, + color = color, + textAlign = TextAlign.Center, + lineHeight = 48.sp, + ) + } } } } @@ -152,10 +181,18 @@ private fun Preview() { var text by remember { mutableStateOf("123") } Column { + PinTextField( + value = text, + onValueChange = { text = it }, + isPasswordVisual = true, + pinTextColor = PinTextColor.Success, + length = 6, + ) PinTextField( value = text, onValueChange = { text = it }, isPasswordVisual = false, + pinTextColor = PinTextColor.Primary, length = 6, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index a302980668..2c45500ae0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -11,16 +11,20 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -35,6 +39,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags @Composable fun SearchBar( @@ -45,6 +50,7 @@ fun SearchBar( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } val interactionSource = remember { MutableInteractionSource() } BasicTextField( @@ -57,7 +63,9 @@ fun SearchBar( } else { state.onActiveChange(false) } - }, + } + .focusRequester(focusRequester) + .testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR), enabled = enabled, value = state.query, onValueChange = state.onQueryChange, @@ -89,6 +97,10 @@ fun SearchBar( ) }, ) + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } } @Suppress("LongParameterList") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index c37307f136..1616968df4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -17,6 +18,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -39,6 +41,7 @@ fun SimpleTextField( textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), placeholderColor: Color = TangemTheme.colors.text.disabled, readOnly: Boolean = false, + centered: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, isValuePasted: Boolean = false, onValuePastedTriggerDismiss: () -> Unit = {}, @@ -80,6 +83,8 @@ fun SimpleTextField( onValuePastedTriggerDismiss() } } + var textStyle = textStyle.copy(color = color) + if (centered) textStyle = textStyle.copy(textAlign = TextAlign.Center) BasicTextField( value = textFieldValue, @@ -91,7 +96,7 @@ fun SimpleTextField( if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text) }, - textStyle = textStyle.copy(color = color), + textStyle = textStyle, cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, readOnly = readOnly, @@ -105,6 +110,7 @@ fun SimpleTextField( value = value, textStyle = textStyle, textValue = textValue, + centered = centered, color = placeholderColor, ) }, @@ -118,10 +124,11 @@ private fun SimpleTextPlaceholder( placeholder: TextReference?, value: String, textStyle: TextStyle, + centered: Boolean, textValue: @Composable () -> Unit, color: Color = TangemTheme.colors.text.disabled, ) { - Box { + Box(contentAlignment = if (centered) Alignment.Center else Alignment.TopStart) { if (value.isBlank() && placeholder != null) { AnimatedContent( targetState = placeholder, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index cf5d0d6153..72111e3c58 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -26,6 +27,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.BaseBlockTestTags /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -64,20 +66,24 @@ fun InputRowDefault( ) { Column( modifier = Modifier - .weight(1f), + .weight(1f) + .testTag(BaseBlockTestTags.BLOCK), ) { title?.let { Text( text = title.resolveReference(), style = TangemTheme.typography.subtitle2, color = titleColor, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing8) + .testTag(BaseBlockTestTags.BLOCK_TITLE), ) } Text( text = text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, + modifier = Modifier.testTag(BaseBlockTestTags.BLOCK_TEXT), ) } iconRes?.let { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index b48f8fd849..259553da0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.CenterEnd @@ -19,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.small.TangemIconButton import com.tangem.core.ui.components.fields.SimpleTextField @@ -69,11 +71,12 @@ fun InputRowRecipient( showDivider: Boolean = false, isLoading: Boolean = false, isValuePasted: Boolean = false, + resolvedAddress: String? = null, ) { val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning } else { - title to TangemTheme.colors.text.secondary + title to TangemTheme.colors.text.tertiary } DividerContainer( modifier = modifier, @@ -144,11 +147,15 @@ fun InputRowRecipient( } else { TangemTheme.colors.text.primary2 }, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8), + modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), ) } } + + ResolvedAddressRow( + isLoading = isLoading, + resolvedAddress = resolvedAddress, + ) } } } @@ -203,6 +210,38 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) { } } +@Composable +private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) { + AnimatedContent( + targetState = if (resolvedAddress.isNullOrBlank() || isLoading) { + ResolvedState.Hide + } else { + ResolvedState.Show(resolvedAddress) + }, + label = "Resolved Address", + ) { state -> + if (state is ResolvedState.Show) { + Column { + HorizontalDivider( + thickness = 0.5.dp, + modifier = Modifier.padding(top = 12.dp, bottom = 12.dp), + color = TangemTheme.colors.stroke.primary, + ) + Text( + text = state.address, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } +} + +private sealed interface ResolvedState { + data object Hide : ResolvedState + data class Show(val address: String) : ResolvedState +} + //region preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -224,6 +263,7 @@ private fun InputRowRecipientPreview( onQrCodeClick = {}, modifier = Modifier.background(TangemTheme.colors.background.primary), isRedesignEnabled = false, + resolvedAddress = value.resolvedAddress, ) } } @@ -232,6 +272,7 @@ private data class InputRowRecipientPreviewData( val value: String, val isError: Boolean, val isLoading: Boolean = false, + val resolvedAddress: String? = null, ) private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider { @@ -250,6 +291,12 @@ private class InputRowRecipientPreviewDataProvider : PreviewParameterProviderFigma + */ +@Composable +fun Label(state: LabelUM, modifier: Modifier = Modifier) { + val backgroundColor by animateColorAsState( + targetValue = when (state.style) { + LabelStyle.ACCENT -> TangemTheme.colors.text.accent.copy(alpha = 0.1f) + LabelStyle.REGULAR -> TangemTheme.colors.control.unchecked + LabelStyle.WARNING -> TangemTheme.colors.text.warning.copy(alpha = 0.1f) + }, + ) + + val textColor by animateColorAsState( + targetValue = when (state.style) { + LabelStyle.ACCENT -> TangemTheme.colors.text.accent + LabelStyle.REGULAR -> TangemTheme.colors.text.secondary + LabelStyle.WARNING -> TangemTheme.colors.text.warning + }, + ) + + AnimatedContent(targetState = state.text) { text -> + Box( + modifier = modifier + .padding(horizontal = 4.dp) + .background( + color = backgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption1, + color = textColor, + ) + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LabelPreview() { + TangemThemePreview { + Column( + modifier = Modifier.padding(16.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + ), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + ), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + ), + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt new file mode 100644 index 0000000000..70b134bfe8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.components.label.entity + +import com.tangem.core.ui.extensions.TextReference + +data class LabelUM( + val text: TextReference, + val style: LabelStyle, +) + +enum class LabelStyle { + REGULAR, ACCENT, WARNING, +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 52bdac7eab..204f816c64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.utils.StringsSigns.DASH_SIGN /** * Market price block @@ -120,7 +120,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { ) } } else { - Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) + Price(price = DASH_SIGN, modifier = priceModifier) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 569384917c..7b1baa0109 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview @@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.NotificationTestTags import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState /** @@ -206,6 +208,7 @@ internal fun TextsBlock( text = titleText, color = titleColor, style = TangemTheme.typography.button, + modifier = Modifier.testTag(NotificationTestTags.TITLE), ) SpacerH(height = TangemTheme.dimens.spacing2) @@ -217,6 +220,7 @@ internal fun TextsBlock( text = subtitleText, color = subtitleColor, style = TangemTheme.typography.caption2, + modifier = Modifier.testTag(NotificationTestTags.TEXT), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt index aa7f0f5d08..ace47189b0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -23,6 +23,7 @@ import androidx.constraintlayout.compose.Visibility import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.audits.AuditLabel import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.badge.Badge @@ -50,8 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { ConstraintLayout( modifier = modifier - .background(TangemTheme.colors.background.action) - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = providerChooseUM.isSelected) .clickable( enabled = !providerChooseUM.hasError(), @@ -133,13 +132,23 @@ private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) { SubcomposeAsyncImage( modifier = modifier .size(40.dp) - .clip(RoundedCornerShape(8.dp)) - .background(TangemColorPalette.Light1), + .clip(RoundedCornerShape(8.dp)), model = ImageRequest.Builder(context = LocalContext.current) .data(iconUrl) .crossfade(enable = true) .allowHardware(false) .build(), + loading = { + RectangleShimmer(radius = 8.dp) + }, + error = { + Box( + modifier = Modifier.background( + color = TangemColorPalette.Light1, + shape = RoundedCornerShape(8.dp), + ), + ) + }, contentDescription = null, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index 942e60deaf..9de699245f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +24,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags @Suppress("LongParameterList") @Composable @@ -54,7 +56,8 @@ fun RoundableCornersRow( .padding( horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12, - ), + ) + .testTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { @@ -63,6 +66,7 @@ fun RoundableCornersRow( color = startTextColor, maxLines = 1, style = startTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_NAME), ) if (iconResId != null && iconClick != null) { Icon( @@ -85,6 +89,7 @@ fun RoundableCornersRow( color = endTextColor, maxLines = 1, style = endTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_VALUE), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 86009a8f7f..be5201fa5c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -2,7 +2,6 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign @@ -22,14 +22,15 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.utils.StringsSigns @Composable fun SelectorRowItem( - @StringRes titleRes: Int, + title: TextReference, @DrawableRes iconRes: Int, modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12), @@ -68,7 +69,8 @@ fun SelectorRowItem( Row( modifier = Modifier .fillMaxWidth() - .padding(paddingValues), + .padding(paddingValues) + .testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -77,7 +79,7 @@ fun SelectorRowItem( contentDescription = null, ) Text( - text = stringResourceSafe(titleRes), + text = title.resolveReference(), style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), @@ -148,7 +150,7 @@ private fun RowScope.SelectorValueContent( private fun SelectorRowItemPreview() { TangemThemePreview { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_slow, + title = resourceReference(R.string.common_fee_selector_option_slow), iconRes = R.drawable.ic_tortoise_24, preDot = TextReference.Str("1000 ETH"), postDot = TextReference.Str("1000 $"), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt index 629f9d10cc..bc292ff0d0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +29,7 @@ import com.tangem.core.ui.components.stories.model.StoryConfig import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapStoriesScreenTestTags import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -101,7 +103,8 @@ inline fun StoriesContainer( interactionSource = remember { MutableInteractionSource() }, indication = LocalIndication.current, onClick = { config.onClose(watchedCounter) }, - ), + ) + .testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index eb7f85d306..87c7602ba9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -94,6 +94,7 @@ fun getActiveIconRes(blockchainId: String): Int { "zklink", "zklink/test" -> R.drawable.img_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 "pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22 + "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 else -> R.drawable.ic_alert_24 } } @@ -186,6 +187,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "zklink", "zklink/test" -> R.drawable.img_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 "pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22 + "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 else -> R.drawable.ic_alert_24 } } @@ -281,6 +283,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "zklink", "zklink/test" -> R.drawable.ic_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22 "pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22 + "hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt deleted file mode 100644 index 2dc5bcd17e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.core.ui.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import timber.log.Timber - -/** - * The ViewModel is scoped to the parent route Navigation graph - * and is provided using the Hilt-generated ViewModel factory - * - * ``` - * val navController = rememberNavController() - * - * navigation( - * route = "parent", - * startDestination = "parent/1" - * ) { - * composable("route/1") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/2") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/3") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * } - * ``` - * - * @param navController NavController within the common NavGraph - * @throws Exception if there is no parent route - */ -@Composable -inline fun NavBackStackEntry.parentHiltViewModel(navController: NavController): T { - val viewModelStoreOwner = remember(this) { - try { - navController.getBackStackEntry(this.destination.parent!!.id) - } catch (e: Exception) { - Timber.tag("scopedViewModel").e(e, "There is no parent route'") - throw e - } - } - - return hiltViewModel(viewModelStoreOwner) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt deleted file mode 100644 index e58126708e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.core.ui.extensions - -import android.R -import android.content.Context -import android.graphics.Color.* -import android.view.WindowManager -import androidx.annotation.ColorRes -import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat -import androidx.fragment.app.Fragment -import kotlin.math.sqrt - -@Deprecated("Use only in legacy fragments") -fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) { - with(requireActivity().window) { - clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - statusBarColor = ContextCompat.getColor(requireContext(), colorResId) - val view = view ?: return - val windowInsetsController = WindowCompat.getInsetsController(this, view) - windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId) - } -} - -// TODO replace by android.graphics.luminance() after bump min API to 24 -@Suppress("MagicNumber") -fun luminance(context: Context, @ColorRes colorRes: Int): Boolean { - val color = context.resources.getColor(colorRes, null) - if (R.color.transparent == color) return true - var rtnValue = false - val rgb = intArrayOf(red(color), green(color), blue(color)) - val brightness = sqrt( - rgb[0] * rgb[0] * .241 + - rgb[1] * rgb[1] * .691 + - rgb[2] * rgb[2] * .068, - ).toInt() - - // color is light - if (brightness >= 200) { - rtnValue = true - } - return rtnValue -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index ef94c6b681..cab83ed0d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -72,26 +71,24 @@ fun Modifier.conditionalCompose( fun Modifier.selectedBorder( isSelected: Boolean, width: Dp = 2.5.dp, - color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + color: Color = TangemTheme.colors.text.accent, radius: Dp = 16.dp, ) = conditionalCompose( condition = isSelected, modifier = { - border( + outsetBorder( width = width, - color = color, - shape = RoundedCornerShape(radius), + color = color.copy(alpha = 0.15f), + shape = RoundedCornerShape(radius + 2.dp), ) - .padding(width) .border( width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(radius - 2.dp), + color = color, + shape = RoundedCornerShape(radius), ) - .clip(RoundedCornerShape(radius - 2.dp)) + .clip(RoundedCornerShape(radius)) }, otherModifier = { - padding(width) - .clip(RoundedCornerShape(radius - 2.dp)) + clip(RoundedCornerShape(radius)) }, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 1834e10f45..aef02f2fe3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -121,7 +121,10 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { +/** + * Returns amount with correct scale + */ +fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 2a78787608..12f15c13b5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -16,6 +16,7 @@ class TangemColors internal constructor( control: Control, stroke: Stroke, field: Field, + overlay: Overlay, ) { var text by mutableStateOf(text) private set @@ -31,6 +32,7 @@ class TangemColors internal constructor( private set var field by mutableStateOf(field) private set + var overlay by mutableStateOf(overlay) @Stable class Text internal constructor( @@ -220,6 +222,22 @@ class TangemColors internal constructor( } } + @Stable + class Overlay internal constructor( + primary: Color, + secondary: Color, + ) { + var primary by mutableStateOf(primary) + private set + var secondary by mutableStateOf(secondary) + private set + + fun update(other: Overlay) { + primary = other.primary + secondary = other.secondary + } + } + fun update(other: TangemColors) { text.update(other.text) icon.update(other.icon) @@ -228,5 +246,6 @@ class TangemColors internal constructor( control.update(other.control) stroke.update(other.stroke) field.update(other.field) + overlay.update(other.overlay) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index b6410efd58..696d1619ee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -4,9 +4,9 @@ import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors -import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color @@ -90,7 +90,7 @@ fun TangemTheme( val rootBackgroundColor = rememberedColors.background.secondary MaterialTheme( - colors = materialThemeColors(colors = themeColors, isDark = isDark), + colorScheme = tangemColorScheme(colors = themeColors), ) { CompositionLocalProvider( LocalTangemColors provides rememberedColors, @@ -143,21 +143,51 @@ object TangemTheme { @Stable @Composable -private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { - return Colors( +private fun tangemColorScheme(colors: TangemColors): ColorScheme { + return ColorScheme( primary = colors.background.primary, - primaryVariant = colors.background.secondary, - secondary = colors.button.primary, - secondaryVariant = colors.text.accent, - background = colors.background.primary, - surface = colors.background.secondary, - error = colors.text.warning, onPrimary = colors.text.primary1, + primaryContainer = colors.background.secondary, + onPrimaryContainer = colors.background.action, + inversePrimary = colors.background.action, + + secondary = colors.button.primary, onSecondary = colors.text.primary1, + secondaryContainer = colors.background.secondary, + onSecondaryContainer = colors.text.primary1, + + tertiary = colors.background.tertiary, + onTertiary = colors.text.tertiary, + tertiaryContainer = colors.background.tertiary, + onTertiaryContainer = colors.text.tertiary, + + background = colors.background.primary, onBackground = colors.text.primary1, + + surface = colors.background.secondary, + surfaceVariant = colors.background.tertiary, onSurface = colors.text.primary1, + onSurfaceVariant = colors.text.secondary, + surfaceTint = colors.background.tertiary, + inverseSurface = colors.button.disabled, + inverseOnSurface = colors.button.primary, + surfaceBright = colors.background.secondary, + surfaceDim = colors.background.tertiary, + surfaceContainer = colors.background.tertiary, + surfaceContainerHigh = colors.background.tertiary, + surfaceContainerHighest = colors.background.tertiary, + surfaceContainerLow = colors.background.tertiary, + surfaceContainerLowest = colors.background.tertiary, + + error = colors.text.warning, + errorContainer = colors.background.tertiary, + onErrorContainer = colors.text.primary2, onError = colors.text.primary2, - isLight = !isDark, + + outline = colors.stroke.primary, + outlineVariant = colors.stroke.secondary, + + scrim = colors.stroke.transparency, ) } @@ -208,6 +238,10 @@ private fun lightThemeColors(): TangemColors { primary = TangemColorPalette.Light1, focused = TangemColorPalette.Light2, ), + overlay = TangemColors.Overlay( + primary = TangemColorPalette.Black.copy(alpha = 0.4f), + secondary = TangemColorPalette.Black.copy(alpha = 0.7f), + ), ) } @@ -258,6 +292,10 @@ private fun darkThemeColors(): TangemColors { primary = TangemColorPalette.Dark5, focused = TangemColorPalette.Dark4, ), + overlay = TangemColors.Overlay( + primary = TangemColorPalette.Black.copy(alpha = 0.4f), + secondary = TangemColorPalette.Black.copy(alpha = 0.7f), + ), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt deleted file mode 100644 index c5c4285292..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.core.ui.screen - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.FloatRange -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Modifier -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemTheme - -/** - * An abstract base class for bottom sheet dialogs that use Compose for UI rendering. - * Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { - - /** - * The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED]. - */ - open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED - - /** - * The fraction of the screen height that the bottom sheet should take when expanded. - * Default is `null`, indicating that the height will be determined by the content. - */ - @FloatRange(from = 0.0, to = 1.0) - open val expandedHeightFraction: Float? = null - - override val screenModifier: Modifier - @Composable - @ReadOnlyComposable - get() = Modifier - .fillMaxWidth() - .let { - if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it - } - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ) - - override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView( - context = inflater.context, - activity = requireActivity(), - overrideSystemBarColors = false, - ) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) - - (dialog as BottomSheetDialog).behavior.apply { - state = initialBottomSheetState - skipCollapsed = true - } - - return dialog - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt deleted file mode 100644 index 48565c82d7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.core.ui.screen - -import android.content.res.Configuration -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import com.tangem.core.ui.R - -/** - * An abstract base class for fragments that use Compose for UI rendering. - * Extends [Fragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeFragment : Fragment(), ComposeScreen { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions() - - return createComposeView(inflater.context, requireActivity()).also { - it.isTransitionGroup = isTransitionsInflated - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - /* - * We need to manually dispatch configuration changes to the Compose view. - * - - * `android:configChanges="uiMode"` is set in the manifest. - * */ - view?.dispatchConfigurationChanged(newConfig) - } - - /** - * Inflates transitions for the fragment. Override this method to customize - * enter and exit transitions for the fragment. - * - * @return `true` if transitions were inflated; `false` otherwise. - */ - protected open fun TransitionInflater.inflateTransitions(): Boolean { - enterTransition = inflateTransition(R.transition.fade) - exitTransition = inflateTransition(R.transition.fade) - - return true - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt new file mode 100644 index 0000000000..b2e4eb5d56 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object BaseBlockTestTags { + const val BLOCK = "BASE_BLOCK" + const val BLOCK_TITLE = "BASE_BLOCK_TITLE" + const val BLOCK_TEXT = "BASE_BLOCK_REWARDS_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt new file mode 100644 index 0000000000..03bfd8f0d3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object BaseButtonTestTags { + const val BUTTON = "BASE_BUTTON" + const val ICON = "BASE_BUTTON_ICON" + const val TEXT = "BASE_BUTTON_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt new file mode 100644 index 0000000000..0f045c5da5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object BuyTokenDetailsScreenTestTags { + const val EXPAND_FIAT_LIST_BUTTON = "BUY_TOKEN_DETAILS_SCREEN_EXPAND_FIAT_LIST_BUTTON" + const val FIAT_CURRENCY_ICON = "BUY_TOKEN_DETAILS_SCREEN_FIAT_CURRENCY_ICON" + const val FIAT_AMOUNT_TEXT_FIELD = "BUY_TOKEN_DETAILS_SCREEN_FIAT_AMOUNT_TEXT_FIELD" + const val TOKEN_AMOUNT = "BUY_TOKEN_DETAILS_SCREEN_TOKEN_AMOUNT" + + const val PROVIDER_LOADING_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE" + const val PROVIDER_LOADING_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE" + + const val PROVIDER_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TITLE" + const val PROVIDER_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TEXT" + + const val TOS_BLOCK = "BUY_TOKEN_DETAILS_SCREEN_TOS_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt new file mode 100644 index 0000000000..a656caf7f0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object BuyTokenFiatListTestTags { + const val LAZY_LIST = "BUY_TOKEN_FIAT_LIST_LAZY_LIST" + const val LAZY_LIST_ITEM = "BUY_TOKEN_FIAT_LIST_LAZY_LIST_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt new file mode 100644 index 0000000000..af41f67814 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object BuyTokenScreenTestTags { + const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST" + const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt index 8f911d8632..0c5eae6e75 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt @@ -2,5 +2,4 @@ package com.tangem.core.ui.test object DialogTestTags { const val DIALOG_CONTAINER = "DIALOG_CONTAINER" - const val BUTTON = "DIALOG_BUTTON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt index b1355ed20a..e5716b476f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt @@ -3,4 +3,5 @@ package com.tangem.core.ui.test object DisclaimerScreenTestTags { const val SCREEN_CONTAINER = "DISCLAIMER_SCREEN_CONTAINER" const val ACCEPT_BUTTON = "DISCLAIMER_SCREEN_ACCEPT_BUTTON" + const val WEB_VIEW = "DISCLAIMER_SCREEN_WEB_VIEW" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt index f635ce4544..dfef1326a7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt @@ -8,4 +8,5 @@ object MainScreenTestTags { const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE" const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM" const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON" + const val MULTI_CURRENCY_ACTION_BUTTON = "MAIN_SCREEN_MULTI_CURRENCY_ACTION_BUTTON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt new file mode 100644 index 0000000000..15a63e2186 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object NotificationTestTags { + const val TITLE = "NOTIFICATION_TITLE" + const val TEXT = "NOTIFICATION_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt new file mode 100644 index 0000000000..55a20fabe0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.test + +object ReferralProgramScreenTestTags { + const val IMAGE = "REFERRAL_PROGRAM_SCREEN_IMAGE" + const val CONDITION_BLOCK = "REFERRAL_PROGRAM_SCREEN_CONDITION_BLOCK" + const val INFO_FOR_YOU_TEXT = "REFERRAL_PROGRAM_SCREEN_INFO_FOR_YOU_TEXT" + const val INFO_FOR_YOUR_FRIEND_TEXT = "REFERRAL_PROGRAM_INFO_FOR_YOUR_FRIEND_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt new file mode 100644 index 0000000000..9ebf2b26e0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object ResidenceSettingsScreenTestTags { + const val COUNTRY_NAME = "RESIDENCE_SETTINGS_SCREEN_COUNTRY_NAME" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt new file mode 100644 index 0000000000..6a2d859e19 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.test + +object SelectCountryBottomSheetTestTags { + + const val LAZY_LIST = "SELECT_COUNTRY_BOTTOM_SHEET_LAZY_LIST" + const val COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ITEM" + const val UNAVAILABLE_COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ITEM" + const val SEARCH_BAR = "SELECT_COUNTRY_BOTTOM_SHEET_SEARCH_BAR" + const val COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ICON" + const val UNAVAILABLE_COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ICON" + const val COUNTRY_NAME = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_NAME" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt new file mode 100644 index 0000000000..3d2af738bd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object SelectNetworkFeeBottomSheetTestTags { + const val READ_MORE_TEXT = "SELECT_NETWORK_FEE_READ_MORE_TEXT" + const val SELECTOR_ITEM = "SELECT_NETWORK_FEE_SELECTOR_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt new file mode 100644 index 0000000000..ebb601e9a5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object SelectPaymentMethodBottomSheetTestTags { + + const val LAZY_LIST = "SELECT_PAYMENT_METHOD_LAZY_LIST" + const val PAYMENT_METHOD_ICON = "PAYMENT_METHOD_NAME" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt new file mode 100644 index 0000000000..d457d18189 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt @@ -0,0 +1,18 @@ +package com.tangem.core.ui.test + +object SelectProviderBottomSheetTestTags { + + const val PAYMENT_METHOD_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_ICON" + const val PAYMENT_METHOD_TITLE = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_TITLE" + const val PAYMENT_METHOD_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_NAME" + const val PAYMENT_METHOD_EXPAND_BUTTON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_EXPAND_BUTTON" + const val TOKEN_AMOUNT = "SELECT_PROVIDER_BOTTOM_SHEET_TOKEN_AMOUNT" + const val AVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_NAME" + const val AVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_ITEM" + const val UNAVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_ITEM" + const val UNAVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_NAME" + const val UNAVAILABLE_PROVIDER_SUBTITLE = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_SUBTITLE" + const val MORE_PROVIDERS_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_ICON" + const val MORE_PROVIDERS_TEXT = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_TEXT" + const val BEST_RATE_LABEL = "SELECT_PROVIDER_BOTTOM_SHEET_BEST_RATE_LABEL" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt new file mode 100644 index 0000000000..f368755c2f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.test + +object StakingDetailsScreenTestTags { + const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + + const val BANNER_IMAGE = "TOKEN_DETAILS_SCREEN_BANNER_IMAGE" + const val BANNER_TEXT = "TOKEN_DETAILS_SCREEN_BANNER_TEXT" + + const val PARAMETER_BLOCK = "STAKING_DETAILS_PARAMETER_BLOCK" + const val PARAMETER_NAME = "STAKING_DETAILS_PARAMETER_NAME" + const val PARAMETER_VALUE = "STAKING_DETAILS_PARAMETER_VALUE" + const val TOS_TEXT = "STAKING_DETAILS_TOS_TEXT" + + const val ACTIVE_STAKING_BLOCK = "STAKING_DETAILS_ACTIVE_STAKING_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt new file mode 100644 index 0000000000..138b27e35b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.test + +object StakingSendDetailsScreenTestTags { + + const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" + const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" + + const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK" + const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt new file mode 100644 index 0000000000..1dc36d3f79 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object StakingSendScreenTestTags { + const val SCREEN_CONTAINER = "STAKING_SEND_SCREEN_CONTAINER" + + const val AMOUNT_CONTAINER_TITLE = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TITLE" + const val AMOUNT_CONTAINER_TEXT = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TEXT" + const val INPUT_TEXT_FIELD = "STAKING_SEND_SCREEN_INPUT_TEXT_FIELD" + const val SECONDARY_AMOUNT = "STAKING_SEND_SCREEN_SECONDARY_AMOUNT" + + const val CURRENCY_BUTTON = "STAKING_SEND_SCREEN_CURRENCY_BUTTON" + const val FIAT_ICON = "STAKING_SEND_SCREEN_FIAT_ICON" + const val CURRENCY_ICON = "STAKING_SEND_SCREEN_CURRENCY_ICON" + const val MAX_BUTTON = "STAKING_SEND_SCREEN_MAX_BUTTON" + const val PREVIOUS_BUTTON = "STAKING_SEND_SCREEN_PREVIOUS_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt new file mode 100644 index 0000000000..40a0ffe621 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object SwapStoriesScreenTestTags { + const val SCREEN_CONTAINER = "SWAP_STORIES_SCREEN_CONTAINER" + const val CLOSE_BUTTON = "SWAP_STORIES_SCREEN_CLOSE_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt new file mode 100644 index 0000000000..cd6cf947d1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.test + +object SwapTokenScreenTestTags { + const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK" + const val BALANCE = "SWAP_TOKEN_SCREEN_BALANCE" + const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" + const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" + const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" + const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" + const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" + const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" + const val TOKEN_NAME = "SWAP_TOKEN_SCREEN_TOKEN_NAME" + const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index 54bc9893ca..bcbee0ae13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -2,4 +2,19 @@ package com.tangem.core.ui.test object TokenDetailsScreenTestTags { const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + + const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE" + const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON" + const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS" + + const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK" + const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK" + const val STAKING_CURRENCY_ICON = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_CURRENCY_ICON" + const val STAKING_SERVICE_TITLE = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TITLE" + const val STAKING_SERVICE_TEXT = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TEXT" + const val STAKING_FIAT_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_FIAT_AMOUNT" + const val STAKING_DOT = "TOKEN_DETAILS_SCREEN_STAKING_DOT" + const val STAKING_TOKEN_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_TOKEN_AMOUNT" + const val STAKING_REWARD_VALUE = "TOKEN_DETAILS_SCREEN_STAKING_REWARD_VALUE" + const val STAKING_CHEVRON_ICON = "TOKEN_DETAILS_SCREEN_STAKING_CHEVRON_ICON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt new file mode 100644 index 0000000000..1c2c1b09bf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object TopAppBarTestTags { + const val TITLE = "TOP_APP_BAR_TITLE" + const val MORE_BUTTON = "TOP_APP_BAR_MORE_BUTTON" + const val CLOSE_BUTTON = "TOP_APP_BAR_CLOSE_BUTTON" +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/AnimatedValue.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt rename to core/ui/src/main/java/com/tangem/core/ui/utils/AnimatedValue.kt index b21726c8e7..20ba1ea49a 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/AnimatedValue.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.compose.extensions +package com.tangem.core.ui.utils import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt deleted file mode 100644 index 744a2d9bf3..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.core.ui.utils - -import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.LOWER_SIGN -import com.tangem.utils.StringsSigns.TILDE_SIGN -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Currency -import java.util.Locale - -@Suppress("LargeClass") -@Deprecated("Use BigDecimal.format") -object BigDecimalFormatter { - - const val EMPTY_BALANCE_SIGN = DASH_SIGN - private const val CAN_BE_LOWER_SIGN = LOWER_SIGN - - private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") - - private const val FIAT_MARKET_DEFAULT_DIGITS = 2 - private const val FIAT_MARKET_EXTENDED_DIGITS = 6 - private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 - - private val usdCurrency = Currency.getInstance("USD") - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmount( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - decimals: Int = FIAT_MARKET_DEFAULT_DIGITS, - locale: Locale = Locale.getDefault(), - withApproximateSign: Boolean = false, - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - - val formatterCurrency = getCurrency(fiatCurrencyCode) - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = decimals - minimumFractionDigits = decimals - roundingMode = RoundingMode.HALF_UP - } - - return if (fiatAmount.checkFiatThreshold()) { - buildString { - append(CAN_BE_LOWER_SIGN) - append( - formatter.format(FIAT_FORMAT_THRESHOLD) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), - ) - } - } else { - val formattedAmount = formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - - if (withApproximateSign) { - buildString { - append(TILDE_SIGN) - append(formattedAmount) - } - } else { - formattedAmount - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmountUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val digits = if (fiatAmount.checkFiatThreshold()) { - FIAT_MARKET_EXTENDED_DIGITS - } else { - FIAT_MARKET_DEFAULT_DIGITS - } - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = digits - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatPriceUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val (formattedAmount, finalScale) = getFiatPriceUncappedWithScale(value = fiatAmount) - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = finalScale - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(formattedAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { - return if (value < BigDecimal.ONE) { - val leadingZeroes = value.scale() - value.precision() - val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES - - val amount = value - .setScale(scale, RoundingMode.HALF_UP) - .stripTrailingZeros() - - amount to amount.scale() - } else { - value to FIAT_MARKET_DEFAULT_DIGITS - } - } - - private fun getCurrency(code: String): Currency { - return runCatching { Currency.getInstance(code) } - .getOrElse { e -> - // Currency code is not valid ISO 4217 code - if (e is IllegalArgumentException) { - usdCurrency - } else { - throw e - } - } - } - - private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt index 105d2416d3..0a3bb10329 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt @@ -1,9 +1,15 @@ package com.tangem.core.ui.utils +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt @Stable @Composable @@ -15,4 +21,16 @@ fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.curr fun Dp.toPx(density: Float): Float = this.value * density -fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) \ No newline at end of file +fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) + +fun Context.dpToPx(dp: Float): Float = dp * resources.displayMetrics.density +fun Context.pxToDp(px: Float): Float = (px / resources.displayMetrics.density).roundToInt().toFloat() + +@Composable +fun Painter.dpSize(): DpSize = DpSize( + intrinsicSize.width.pxToDp().dp, + intrinsicSize.height.pxToDp().dp, +) + +@Composable +private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBitmap.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt rename to core/ui/src/main/java/com/tangem/core/ui/utils/ImageBitmap.kt index 99911d9e6f..4f4cff9559 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBitmap.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.compose.extensions +package com.tangem.core.ui.utils import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources diff --git a/app/src/main/res/drawable-hdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-hdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-hdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-mdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-mdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-mdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xhdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-xhdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-xhdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-xxhdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-xxhdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable/currency0.webp b/core/ui/src/main/res/drawable/currency0.webp similarity index 100% rename from app/src/main/res/drawable/currency0.webp rename to core/ui/src/main/res/drawable/currency0.webp diff --git a/app/src/main/res/drawable/currency1.webp b/core/ui/src/main/res/drawable/currency1.webp similarity index 100% rename from app/src/main/res/drawable/currency1.webp rename to core/ui/src/main/res/drawable/currency1.webp diff --git a/app/src/main/res/drawable/currency2.webp b/core/ui/src/main/res/drawable/currency2.webp similarity index 100% rename from app/src/main/res/drawable/currency2.webp rename to core/ui/src/main/res/drawable/currency2.webp diff --git a/app/src/main/res/drawable/currency3.webp b/core/ui/src/main/res/drawable/currency3.webp similarity index 100% rename from app/src/main/res/drawable/currency3.webp rename to core/ui/src/main/res/drawable/currency3.webp diff --git a/app/src/main/res/drawable/currency4.webp b/core/ui/src/main/res/drawable/currency4.webp similarity index 100% rename from app/src/main/res/drawable/currency4.webp rename to core/ui/src/main/res/drawable/currency4.webp diff --git a/app/src/main/res/drawable/dapps1.webp b/core/ui/src/main/res/drawable/dapps1.webp similarity index 100% rename from app/src/main/res/drawable/dapps1.webp rename to core/ui/src/main/res/drawable/dapps1.webp diff --git a/app/src/main/res/drawable/dapps2.webp b/core/ui/src/main/res/drawable/dapps2.webp similarity index 100% rename from app/src/main/res/drawable/dapps2.webp rename to core/ui/src/main/res/drawable/dapps2.webp diff --git a/app/src/main/res/drawable/dapps3.webp b/core/ui/src/main/res/drawable/dapps3.webp similarity index 100% rename from app/src/main/res/drawable/dapps3.webp rename to core/ui/src/main/res/drawable/dapps3.webp diff --git a/app/src/main/res/drawable/dapps4.webp b/core/ui/src/main/res/drawable/dapps4.webp similarity index 100% rename from app/src/main/res/drawable/dapps4.webp rename to core/ui/src/main/res/drawable/dapps4.webp diff --git a/app/src/main/res/drawable/dapps5.webp b/core/ui/src/main/res/drawable/dapps5.webp similarity index 100% rename from app/src/main/res/drawable/dapps5.webp rename to core/ui/src/main/res/drawable/dapps5.webp diff --git a/core/ui/src/main/res/drawable/ic_archive_24.xml b/core/ui/src/main/res/drawable/ic_archive_24.xml new file mode 100644 index 0000000000..919033523a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_archive_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_hyperliquid_22.xml b/core/ui/src/main/res/drawable/ic_hyperliquid_22.xml new file mode 100644 index 0000000000..7f7c511fa5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hyperliquid_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml new file mode 100644 index 0000000000..101239eb7d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml new file mode 100644 index 0000000000..b610863fbc --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml new file mode 100644 index 0000000000..ecf6f9754c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_stack_new_24.xml b/core/ui/src/main/res/drawable/ic_stack_new_24.xml new file mode 100644 index 0000000000..a0a35fc823 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_stack_new_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tangem_logo.xml b/core/ui/src/main/res/drawable/ic_tangem_logo.xml similarity index 100% rename from app/src/main/res/drawable/ic_tangem_logo.xml rename to core/ui/src/main/res/drawable/ic_tangem_logo.xml diff --git a/core/ui/src/main/res/drawable/ic_warning_16.xml b/core/ui/src/main/res/drawable/ic_warning_16.xml new file mode 100644 index 0000000000..ff30f1ba7e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_warning_16.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/img_card_placeholder_wallet_2.webp b/core/ui/src/main/res/drawable/img_card_placeholder_wallet_2.webp similarity index 100% rename from app/src/main/res/drawable/img_card_placeholder_wallet_2.webp rename to core/ui/src/main/res/drawable/img_card_placeholder_wallet_2.webp diff --git a/core/ui/src/main/res/drawable/img_hyperliquid_22.xml b/core/ui/src/main/res/drawable/img_hyperliquid_22.xml new file mode 100644 index 0000000000..9e66c2bd9a --- /dev/null +++ b/core/ui/src/main/res/drawable/img_hyperliquid_22.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 534b602d2c..98ba454b64 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -9,6 +9,7 @@ object StringsSigns { const val LOWER_SIGN = "<" const val TILDE_SIGN = "~" const val COMA_SIGN = "," + const val POINT_SIGN = "." const val INFINITY_SIGN = "āˆž" const val NON_BREAKING_SPACE = '\u00A0' const val PERCENT = "%" diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt deleted file mode 100644 index 357b26203c..0000000000 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.utils.extensions - -/** -[REDACTED_AUTHOR] - */ -fun Int.isEven(): Boolean = this % 2 == 0 \ No newline at end of file diff --git a/data/account/.gitignore b/data/account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts new file mode 100644 index 0000000000..464ba8486e --- /dev/null +++ b/data/account/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.account" +} + +dependencies { + + // region Project - Core + api(projects.core.utils) + // endregion + + // region Project - Domain + api(projects.domain.account) + api(projects.domain.models) + // endregion + + // Project - Data + implementation(projects.core.datasource) + // endregion + + // region DI + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + // endregion + + // region Other Dependencies + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + // endregion +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt new file mode 100644 index 0000000000..e05c402f68 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -0,0 +1,25 @@ +package com.tangem.data.account.di + +import com.tangem.data.account.repository.DefaultAccountsCRUDRepository +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.repository.AccountsCRUDRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AccountDataModule { + + @Provides + @Singleton + fun provideAccountsCRUDRepository(userWalletsStore: UserWalletsStore): AccountsCRUDRepository { + return DefaultAccountsCRUDRepository( + runtimeStore = RuntimeSharedStore(), + userWalletsStore = userWalletsStore, + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt new file mode 100644 index 0000000000..a9fba30f57 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -0,0 +1,90 @@ +package com.tangem.data.account.repository + +import arrow.core.Option +import arrow.core.Option.Companion.catch +import arrow.core.none +import arrow.core.raise.option +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.* +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** +[REDACTED_AUTHOR] + */ +// TODO: [REDACTED_JIRA] +internal class DefaultAccountsCRUDRepository( + private val runtimeStore: RuntimeSharedStore>, + private val userWalletsStore: UserWalletsStore, +) : AccountsCRUDRepository { + + override suspend fun getAccounts(userWalletId: UserWalletId): Option = catch { + runtimeStore.getSyncOrNull() + ?.firstOrNull { it.userWallet.walletId == userWalletId } + ?: return none() + } + + override suspend fun getAccount(accountId: AccountId): Option = catch { + runtimeStore.getSyncOrNull().orEmpty() + .flatMap { it.accounts } + .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio + ?: return none() + } + + override suspend fun getArchivedAccount(accountId: AccountId): Option = option { + createMockArchivedAccount(userWalletId = accountId.userWalletId) + } + + override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> = option { + listOf( + createMockArchivedAccount(userWalletId), + ) + } + + override fun getArchivedAccounts(userWalletId: UserWalletId): Flow> { + return flow { + getArchivedAccountsSync(userWalletId).getOrNull().orEmpty() + } + } + + override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit + + override suspend fun saveAccounts(accountList: AccountList) { + runtimeStore.update(emptyList()) { + it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } + } + } + + override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int { + val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1 + + return activeAccountsCount + 1 + } + + override fun getUserWallet(userWalletId: UserWalletId): UserWallet { + return userWalletsStore.getSyncStrict(userWalletId) + } + + private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount { + val derivationIndex = DerivationIndex(value = 1000).getOrNull()!! + + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + tokensCount = 2, + networksCount = 1, + ) + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index bd48e9cea2..9a57b58ea2 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -6,11 +6,11 @@ import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import timber.log.Timber import javax.inject.Inject @@ -111,6 +111,7 @@ class NetworkFactory @Inject constructor( hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, canHandleTokens = canHandleTokens, transactionExtrasType = blockchain.getSupportedTransactionExtras(), + nameResolvingType = blockchain.getNameResolvingType(), ) } .getOrNull() @@ -323,11 +324,19 @@ class NetworkFactory @Inject constructor( Blockchain.Scroll, Blockchain.ScrollTestnet, Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet, Blockchain.Pepecoin, Blockchain.PepecoinTestnet, + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet, -> Network.TransactionExtrasType.NONE // endregion } } + private fun Blockchain.getNameResolvingType(): Network.NameResolvingType { + return when (this) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS + else -> Network.NameResolvingType.NONE + } + } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun createNetworkStandardType(blockchain: Blockchain) = getNetworkStandardType(blockchain) diff --git a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt index 766643ffe0..4cbcb9a0a5 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt @@ -7,10 +7,10 @@ import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.common.test.utils.ProvideTestModels -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt index 107cf20e9f..7fecd6950c 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -10,19 +10,21 @@ import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.filterIf import timber.log.Timber internal class DefaultExpressRepository( private val tangemExpressApi: TangemExpressApi, private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, ) : ExpressRepository { override suspend fun getProviders( userWallet: UserWallet, filterProviderTypes: List, - ): List { - return safeApiCall( + ): List = with(dispatchers.io) { + safeApiCall( call = { tangemExpressApi.getProviders( userWalletId = userWallet.walletId.stringValue, diff --git a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt index 75bfe50bf5..299b631162 100644 --- a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt +++ b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.express.ExpressErrorResolver import com.tangem.domain.express.ExpressRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,10 +35,12 @@ internal object ExpressDataModule { fun provideExpressRepository( tangemExpressApi: TangemExpressApi, appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, ): ExpressRepository { return DefaultExpressRepository( tangemExpressApi = tangemExpressApi, appPreferencesStore = appPreferencesStore, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index c0da8f1be9..9095c4f1ab 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.manageTokens) implementation(projects.domain.card) + implementation(projects.domain.wallets) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.legacy) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index d311b44167..34beb0198c 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -22,8 +22,8 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -247,26 +247,43 @@ internal class DefaultCustomTokensRepository( val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "User wallet [$userWalletId] not found while getting supported networks" } - val scanResponse = userWallet.requireColdWallet().scanResponse // TODO [REDACTED_TASK_KEY] - Blockchain.entries - .mapNotNull { blockchain -> - val canHandleBlockchain = scanResponse.card.canHandleBlockchain( - blockchain, - scanResponse.cardTypesResolver, - excludedBlockchains, - ) + when (userWallet) { + is UserWallet.Hot -> { + Blockchain.entries.mapNotNull { + // TODO: refactor [REDACTED_JIRA]\ + if (it.isTestnet() || it in excludedBlockchains) return@mapNotNull null - if (canHandleBlockchain) { networkFactory.create( - blockchain = blockchain, + blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) - } else { - null } } + is UserWallet.Cold -> { + val scanResponse = userWallet.scanResponse + + Blockchain.entries + .mapNotNull { blockchain -> + val canHandleBlockchain = scanResponse.card.canHandleBlockchain( + blockchain, + scanResponse.cardTypesResolver, + excludedBlockchains, + ) + + if (canHandleBlockchain) { + networkFactory.create( + blockchain = blockchain, + extraDerivationPath = null, + userWallet = userWallet, + ) + } else { + null + } + } + } + } } override fun createDerivationPath(rawPath: String): Network.DerivationPath { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index ce7a1c066a..eca48639fe 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -206,11 +206,10 @@ internal class DefaultManageTokensRepository( ) private fun getSupportedBlockchains(userWallet: UserWallet?): List { - return (userWallet as? UserWallet.Cold)?.scanResponse?.let { - it.card.supportedBlockchains(it.cardTypesResolver, excludedBlockchains) // TODO [REDACTED_TASK_KEY] - } ?: Blockchain.entries.filter { - !it.isTestnet() && it !in excludedBlockchains - } + return userWallet?.supportedBlockchains(excludedBlockchains) + ?: Blockchain.entries.filter { + !it.isTestnet() && it !in excludedBlockchains + } } // endregion diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 444a4b5bb6..d785c4e2b6 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -13,14 +13,14 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig -import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber internal class ManagedCryptoCurrencyFactory( diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 585b0a640a..7ceb082dd5 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -17,12 +17,10 @@ import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections @@ -542,11 +540,10 @@ internal class DefaultNFTRepository @Inject constructor( } private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean { - // TODO [REDACTED_TASK_KEY] - val scanResponse = userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().scanResponse + val userWallet = userWalletsStore.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(backendId) ?: return false return blockchain.canHandleNFTs() && - scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains) + userWallet.canHandleToken(blockchain, excludedBlockchains) } } \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 45d62bced2..d386676472 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -1,48 +1,24 @@ package com.tangem.data.notifications -import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.* -import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.getSyncOrNull +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.notifications.models.NotificationsEligibleNetwork -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import javax.inject.Inject -internal class DefaultNotificationsRepository @Inject constructor( - private val tangemTechApi: TangemTechApi, - private val appInfoProvider: AppInfoProvider, +class DefaultNotificationsRepository @Inject constructor( private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, ) : NotificationsRepository { - override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { - tangemTechApi.createApplicationId( - NotificationApplicationCreateBody( - platform = appInfoProvider.platform.lowercase(), - device = appInfoProvider.device, - systemVersion = appInfoProvider.osVersion, - language = appInfoProvider.language, - timezone = appInfoProvider.timezone, - version = appInfoProvider.appVersion, - pushToken = pushToken, - ), - ).getOrThrow().appId.let(::ApplicationId) + override suspend fun shouldShowNotification(key: String): Boolean { + return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true) } - override suspend fun saveApplicationId(appId: ApplicationId) { - appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) - } - - override suspend fun getApplicationId(): ApplicationId? { - return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) - ?.let(::ApplicationId) + override suspend fun setShouldShowNotifications(key: String, value: Boolean) { + appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value) } override suspend fun incrementTronTokenFeeNotificationShowCounter() { @@ -62,27 +38,6 @@ internal class DefaultNotificationsRepository @Inject constructor( ) } - override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { - withContext(dispatchers.io) { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = appInfoProvider.osVersion, - language = appInfoProvider.language, - timezone = appInfoProvider.timezone, - version = appInfoProvider.appVersion, - ), - ).getOrThrow() - } - } - - override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { - tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { - NotificationsEligibleNetworkConverter.convert(it) - } - } - override suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean { return appPreferencesStore.getSyncOrNull( key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt new file mode 100644 index 0000000000..b881aff0c5 --- /dev/null +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt @@ -0,0 +1,68 @@ +package com.tangem.data.notifications + +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.* +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.repository.PushNotificationsRepository +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultPushNotificationsRepository @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val appInfoProvider: AppInfoProvider, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : PushNotificationsRepository { + + override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = appInfoProvider.platform.lowercase(), + device = appInfoProvider.device, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + version = appInfoProvider.appVersion, + pushToken = pushToken, + ), + ).getOrThrow().appId.let(::ApplicationId) + } + + override suspend fun saveApplicationId(appId: ApplicationId) { + appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) + } + + override suspend fun getApplicationId(): ApplicationId? { + return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + ?.let(::ApplicationId) + } + + override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { + withContext(dispatchers.io) { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + version = appInfoProvider.appVersion, + ), + ).getOrThrow() + } + } + + override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { + tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { + NotificationsEligibleNetworkConverter.convert(it) + } + } +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt index 555ac66802..18f26e5763 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.notifications.di import com.tangem.data.notifications.DefaultNotificationsRepository +import com.tangem.data.notifications.DefaultPushNotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -12,6 +14,10 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface NotificationsModule { + @Binds + @Singleton + fun bindPushNotificationsRepository(repository: DefaultPushNotificationsRepository): PushNotificationsRepository + @Binds @Singleton fun bindNotificationsRepository(repository: DefaultNotificationsRepository): NotificationsRepository diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 13d4dbb6b1..a92861eca6 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -1,30 +1,21 @@ package com.tangem.data.notifications -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.stringPreferencesKey -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.utils.info.AppInfoProvider +import com.google.common.truth.Truth.assertThat import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.google.common.truth.Truth.assertThat -import com.squareup.moshi.Moshi -import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.tangemTech.models.* -import com.tangem.domain.notifications.models.ApplicationId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import androidx.datastore.preferences.core.Preferences +import com.squareup.moshi.Moshi +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.flowOf class DefaultNotificationsRepositoryTest { - private val tangemTechApi: TangemTechApi = mockk() - private val appInfoProvider: AppInfoProvider = mockk() private val preferencesDataStore: DataStore = mockk() private val appPreferencesStore = AppPreferencesStore( moshi = Moshi.Builder().build(), @@ -32,147 +23,77 @@ class DefaultNotificationsRepositoryTest { preferencesDataStore = preferencesDataStore, ) private val repository = DefaultNotificationsRepository( - tangemTechApi = tangemTechApi, - appInfoProvider = appInfoProvider, appPreferencesStore = appPreferencesStore, - dispatchers = TestingCoroutineDispatcherProvider(), ) @Test - fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + fun `GIVEN shouldShowNotification returns true WHEN called THEN returns true`() = runTest { // GIVEN - val pushToken = "test-push-token" - val expectedAppId = ApplicationId("test-app-id") - val expectedAppIdResponse = NotificationApplicationIdResponse( - appId = expectedAppId.value, - ) - coEvery { appInfoProvider.platform } returns "android" - coEvery { appInfoProvider.device } returns "test-device" - coEvery { appInfoProvider.osVersion } returns "11" - coEvery { appInfoProvider.language } returns "en" - coEvery { appInfoProvider.appVersion } returns "5.21.1" - coEvery { appInfoProvider.timezone } returns "UTC" - coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( - expectedAppIdResponse, - ) + val key = "test-key" + val preferences = mockk(relaxed = true) + every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns true + coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - val result = repository.createApplicationId(pushToken) + val result = repository.shouldShowNotification(key) // THEN - assertThat(result).isEqualTo(expectedAppId) - coVerify { - tangemTechApi.createApplicationId( - NotificationApplicationCreateBody( - platform = "android", - device = "test-device", - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - pushToken = pushToken, - ), - ) - } + assertThat(result).isTrue() } @Test - fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + fun `GIVEN shouldShowNotification returns false WHEN called THEN returns false`() = runTest { // GIVEN - val appId = ApplicationId("test-app-id") + val key = "test-key" val preferences = mockk(relaxed = true) - coEvery { preferencesDataStore.updateData(any()) } returns preferences + every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns false + coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - repository.saveApplicationId(appId) + val result = repository.shouldShowNotification(key) + + // THEN + assertThat(result).isFalse() + } + + @Test + fun `GIVEN setShouldShowNotifications WHEN called THEN stores value in preferences`() = runTest { + // GIVEN + val key = "test-key" + val value = false + coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true) + + // WHEN + repository.setShouldShowNotifications(key, value) // THEN coVerify { preferencesDataStore.updateData(any()) } } @Test - fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + fun `GIVEN incrementTronTokenFeeNotificationShowCounter WHEN called THEN increments counter`() = runTest { // GIVEN - val expectedAppId = ApplicationId("test-app-id") + coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true) + + // WHEN + repository.incrementTronTokenFeeNotificationShowCounter() + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN getTronTokenFeeNotificationShowCounter WHEN called THEN returns counter value`() = runTest { + // GIVEN + val expectedCount = 5 val preferences = mockk(relaxed = true) - val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) - every { preferences[key] } returns expectedAppId.value + every { preferences[PreferencesKeys.TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY] } returns expectedCount coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - val result = repository.getApplicationId() + val result = repository.getTronTokenFeeNotificationShowCounter() // THEN - assertThat(result).isEqualTo(expectedAppId) - } - - @Test - fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { - // GIVEN - val appId = ApplicationId("test-app-id") - val pushToken = "test-push-token" - coEvery { appInfoProvider.device } returns "test-device" - coEvery { appInfoProvider.osVersion } returns "11" - coEvery { appInfoProvider.language } returns "en" - coEvery { appInfoProvider.appVersion } returns "5.21.1" - coEvery { appInfoProvider.timezone } returns "UTC" - coEvery { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - ), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.sendPushToken(appId, pushToken) - - // THEN - coVerify { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - ), - ) - } - } - - @Test - fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { - // GIVEN - val expectedNetworks = listOf( - CryptoNetworkResponse( - id = 1, - name = "Ethereum", - networkId = "ethereum", - ), - CryptoNetworkResponse( - id = 2, - name = "Bitcoin", - networkId = "bitcoin", - ), - ) - coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( - expectedNetworks, - ) - - // WHEN - val result = repository.getEligibleNetworks() - - // THEN - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) - assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) - coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + assertThat(result).isEqualTo(expectedCount) } } \ No newline at end of file diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt new file mode 100644 index 0000000000..b423cb27ed --- /dev/null +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt @@ -0,0 +1,178 @@ +package com.tangem.data.notifications + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPushNotificationsRepositoryTest { + private val tangemTechApi: TangemTechApi = mockk() + private val appInfoProvider: AppInfoProvider = mockk() + private val preferencesDataStore: DataStore = mockk() + private val appPreferencesStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = preferencesDataStore, + ) + private val repository = DefaultPushNotificationsRepository( + tangemTechApi = tangemTechApi, + appInfoProvider = appInfoProvider, + appPreferencesStore = appPreferencesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + // GIVEN + val pushToken = "test-push-token" + val expectedAppId = ApplicationId("test-app-id") + val expectedAppIdResponse = NotificationApplicationIdResponse( + appId = expectedAppId.value, + ) + coEvery { appInfoProvider.platform } returns "android" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.appVersion } returns "5.21.1" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( + expectedAppIdResponse, + ) + + // WHEN + val result = repository.createApplicationId(pushToken) + + // THEN + assertThat(result).isEqualTo(expectedAppId) + coVerify { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = "android", + device = "test-device", + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + pushToken = pushToken, + ), + ) + } + } + + @Test + fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val preferences = mockk(relaxed = true) + coEvery { preferencesDataStore.updateData(any()) } returns preferences + + // WHEN + repository.saveApplicationId(appId) + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + // GIVEN + val expectedAppId = ApplicationId("test-app-id") + val preferences = mockk(relaxed = true) + val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) + every { preferences[key] } returns expectedAppId.value + coEvery { preferencesDataStore.data } returns flowOf(preferences) + + // WHEN + val result = repository.getApplicationId() + + // THEN + assertThat(result).isEqualTo(expectedAppId) + } + + @Test + fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val pushToken = "test-push-token" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.appVersion } returns "5.21.1" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + ), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.sendPushToken(appId, pushToken) + + // THEN + coVerify { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + ), + ) + } + } + + @Test + fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { + // GIVEN + val expectedNetworks = listOf( + CryptoNetworkResponse( + id = 1, + name = "Ethereum", + networkId = "ethereum", + ), + CryptoNetworkResponse( + id = 2, + name = "Bitcoin", + networkId = "bitcoin", + ), + ) + coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( + expectedNetworks, + ) + + // WHEN + val result = repository.getEligibleNetworks() + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) + assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) + coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + } +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 8cda8c8835..cfffffb159 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -22,7 +22,6 @@ import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency @@ -170,23 +169,17 @@ internal class DefaultHotCryptoRepository( // TODO: [REDACTED_JIRA] private fun UserWallet.canHandleHotCrypto(hotToken: HotCryptoResponse.Token): Boolean { - if (this !is UserWallet.Cold) { - return true // TODO [REDACTED_TASK_KEY] - } - val isToken = hotToken.contractAddress != null && hotToken.decimalCount != null val blockchain = hotToken.networkId?.let { Blockchain.fromNetworkId(it) } ?: return false return if (isToken) { - scanResponse.card.canHandleToken( + canHandleToken( blockchain = blockchain, - cardTypesResolver = cardTypesResolver, excludedBlockchains = excludedBlockchains, ) } else { - scanResponse.card.canHandleBlockchain( + canHandleBlockchain( blockchain = blockchain, - cardTypesResolver = cardTypesResolver, excludedBlockchains = excludedBlockchains, ) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index 2291dbc4c6..486ba69104 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -158,5 +158,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> null Blockchain.KaspaTestnet -> null Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt index 582fbead56..95beb54a57 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.datasource.api.onramp.models.response.Status import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt index 692d5cb24a..4adc0ed55a 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt @@ -7,10 +7,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.* /** * Default implementation of [SingleQuoteStatusProducer] @@ -28,7 +25,7 @@ internal class DefaultSingleQuoteStatusProducer @AssistedInject constructor( override fun produce(): Flow { return quotesStatusesStore.get() - .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } + .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } ?: fallback } .distinctUntilChanged() .flowOn(dispatchers.default) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index fbdea80804..0dad5046fe 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -10,8 +10,6 @@ import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.blockchainsdk.utils.toMigratedCoinId import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.staking.converters.YieldConverter @@ -22,7 +20,6 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionConverte import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi @@ -32,22 +29,22 @@ import com.tangem.datasource.api.stakekit.models.response.model.action.StakingAc import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter -import com.tangem.datasource.local.token.converter.TokenConverter +import com.tangem.datasource.local.token.converter.YieldTokenConverter import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction @@ -62,8 +59,6 @@ import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import timber.log.Timber -import java.math.BigDecimal -import kotlin.time.Duration.Companion.seconds @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( @@ -74,7 +69,6 @@ internal class DefaultStakingRepository( private val walletManagersFacade: WalletManagersFacade, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, - private val stakingIdFactory: StakingIdFactory, moshi: Moshi, ) : StakingRepository { @@ -95,10 +89,6 @@ internal class DefaultStakingRepository( private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) } private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) } - override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? { - return stakingIdFactory.createIntegrationId(currencyId = cryptoCurrencyId) - } - override suspend fun fetchEnabledYields() { withContext(dispatchers.io) { when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) { @@ -202,7 +192,7 @@ internal class DefaultStakingRepository( return@channelFlow } - val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() + val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null getEnabledYields() .distinctUntilChanged() @@ -248,7 +238,7 @@ internal class DefaultStakingRepository( return StakingAvailability.Unavailable } - val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() + val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null val yields = getEnabledYieldsSync() if (yields.isEmpty()) { @@ -382,32 +372,6 @@ internal class DefaultStakingRepository( } } - override suspend fun getSingleYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): YieldBalance { - val stakingId = stakingIdFactory.create( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ) ?: error("Could not create stakingId") - - return stakingBalanceStoreV2.getSyncOrNull(userWalletId = userWalletId, stakingId = stakingId) - ?: YieldBalance.Error(integrationId = stakingId.integrationId, address = stakingId.address) - } - - override suspend fun getMultiYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): List? { - val stakingIds = cryptoCurrencies.mapNotNull { - stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) - } - - return stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) - ?.filter { it.getStakingId() in stakingIds } - } - override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { return withContext(dispatchers.default) { val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false @@ -422,14 +386,6 @@ internal class DefaultStakingRepository( } } - override fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? { - return when { - stakingIdFactory.isPolygonIntegrationId(integrationId) && - stakingActionType == StakingActionType.CLAIM_REWARDS -> BigDecimal.ONE - else -> null - } - } - private suspend fun createActionRequestBody( userWalletId: UserWalletId, network: Network, @@ -443,7 +399,7 @@ internal class DefaultStakingRepository( ), args = ActionRequestBodyArgs( amount = params.amount.toPlainString(), - inputToken = TokenConverter.convertBack(params.token), + inputToken = YieldTokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, validatorAddresses = listOf(params.validatorAddress), // check on other networks tronResource = getTronResource(network), @@ -481,17 +437,6 @@ internal class DefaultStakingRepository( } } - override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { - val integrationId = stakingIdFactory.createIntegrationId(currencyId = cryptoCurrency.id) - - return when (integrationId) { - Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(), - Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId(), - -> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) - else -> StakingApproval.Empty - } - } - private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data { return when (Blockchain.fromId(networkId)) { Blockchain.Solana, @@ -543,14 +488,7 @@ internal class DefaultStakingRepository( } } - @Suppress("unused") companion object { - private const val YIELDS_STORE_KEY = "yields" - - private const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" - - internal val YIELDS_WATITING_TIMEOUT = 15.seconds - private val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79") } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 9d8c056e83..60fe5f9e3b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO -import com.tangem.datasource.local.token.converter.TokenConverter +import com.tangem.datasource.local.token.converter.YieldTokenConverter import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule @@ -24,8 +24,8 @@ internal object YieldConverter : Converter { override fun convert(value: YieldDTO): Yield { return Yield( id = value.id.asMandatory("id"), - token = TokenConverter.convert(value.token.asMandatory("token")), - tokens = value.tokens.asMandatory("tokens").map(TokenConverter::convert), + token = YieldTokenConverter.convert(value.token.asMandatory("token")), + tokens = value.tokens.asMandatory("tokens").map(YieldTokenConverter::convert), args = convertArgs(value.args.asMandatory("args")), status = convertStatus(value.status.asMandatory("status")), apy = value.apy.asMandatory("apy"), @@ -91,9 +91,9 @@ internal object YieldConverter : Converter { logoUri = metadataDTO.logoUri.asMandatory("logoUri"), description = metadataDTO.description.asMandatory("description"), documentation = metadataDTO.documentation, - gasFeeToken = TokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), - token = TokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), - tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(TokenConverter::convert), + gasFeeToken = YieldTokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), + token = YieldTokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), + tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(YieldTokenConverter::convert), type = metadataDTO.type.asMandatory("type"), rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")), cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) }, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt index a9fb76fc0a..64ed35b3e1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.transaction import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO -import com.tangem.datasource.local.token.converter.TokenConverter +import com.tangem.datasource.local.token.converter.YieldTokenConverter import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.utils.converter.Converter @@ -10,7 +10,7 @@ internal object GasEstimateConverter : Converter { Timber.i("Start fetching yield balances for params:\n$params") - checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { - return it.left() + val stakingIds = params.stakingIds.ifEmpty { + Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") + return Unit.right() } - val stakingIds = getStakingIds(params).getOrElse { + checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { return it.left() } @@ -94,30 +89,6 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( } } - private suspend fun getStakingIds(params: MultiYieldBalanceFetcher.Params) = either { - val stakingIds = catch( - block = { - params.currencyIdWithNetworkMap.mapNotNullTo(hashSetOf()) { (currencyId, network) -> - stakingIdFactory.create( - userWalletId = params.userWalletId, - currencyId = currencyId, - network = network, - ) - } - }, - catch = ::raise, - ) - - ensure(stakingIds.isNotEmpty()) { - val exception = IllegalStateException("Unable to create staking ids for $params: list is empty") - Timber.e(exception) - - raise(exception) - } - - stakingIds - } - private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set): Set { val yieldIds = getYieldsIds(userWalletId = userWalletId) @@ -178,7 +149,9 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( // TODO: in the future, consider optimizing this part .chunked(size = 15) // StakeKitApi limitation: no more than 15 requests at the same time .map { - async(dispatchers.io) { stakeKitApi.getMultipleYieldBalances(it).bind() } + async(dispatchers.io) { + stakeKitApi.getMultipleYieldBalances(it).bind() + } } .awaitAll() .flatten() diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt index 533610817a..09e9f415de 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducer.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.multi import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt index 4f507e4739..1666b20e35 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt @@ -20,9 +20,7 @@ internal class DefaultSingleYieldBalanceFetcher @Inject constructor( return multiYieldBalanceFetcher( params = MultiYieldBalanceFetcher.Params( userWalletId = params.userWalletId, - currencyIdWithNetworkMap = mapOf( - params.currencyId to params.network, - ), + stakingIds = setOf(params.stakingId), ), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt index bd38f407a4..9abbd5db14 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -2,9 +2,7 @@ package com.tangem.data.staking.single import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.data.staking.utils.StakingIdFactory -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer @@ -24,7 +22,7 @@ import timber.log.Timber * * @property params params * @property multiYieldBalanceSupplier multi yield balance supplier - * @property stakingIdFactory factory for creating [StakingID] + * @property analyticsExceptionHandler analytics exception handler * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -32,20 +30,14 @@ import timber.log.Timber internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( @Assisted private val params: SingleYieldBalanceProducer.Params, private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, - private val stakingIdFactory: StakingIdFactory, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, ) : SingleYieldBalanceProducer { override val fallback: YieldBalance by lazy { - YieldBalance.Error( - integrationId = stakingIdFactory.createIntegrationId(currencyId = params.currencyId), - address = null, - ) + YieldBalance.Error(stakingId = params.stakingId) } - private var stakingId: StakingID? = null - override fun produce(): Flow { Timber.i("Producing yield balance for params:\n$params") @@ -53,14 +45,9 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), ) .mapNotNull { balances -> - val currentStakingId = getStakingId() + val currentStakingId = params.stakingId - if (currentStakingId == null) { - Timber.i("Staking ID is null for params: $params") - return@mapNotNull YieldBalance.Unsupported - } - - val currentBalances = balances.filter { it.getStakingId() == currentStakingId } + val currentBalances = balances.filter { it.stakingId == currentStakingId } if (currentBalances.size > 1) { analyticsExceptionHandler.sendException( @@ -86,34 +73,16 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( currentBalances.first() } } else { - val balance = currentBalances.firstOrNull() + val balance = currentBalances.firstOrNull() ?: return@mapNotNull null - if (balance != null) { - Timber.i("Yield balance found for $currentStakingId:\n$balance") - balance - } else { - Timber.i("No yield balance found for $currentStakingId:\n${YieldBalance.Unsupported}") - YieldBalance.Unsupported - } + Timber.i("Yield balance found for $currentStakingId:\n$balance") + balance } } .distinctUntilChanged() .flowOn(dispatchers.default) } - private suspend fun getStakingId(): StakingID? { - val saved = stakingId - - if (saved != null) return saved - - return stakingIdFactory.create( - userWalletId = params.userWalletId, - currencyId = params.currencyId, - network = params.network, - ) - .also { stakingId = it } - } - @AssistedFactory interface Factory : SingleYieldBalanceProducer.Factory { override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt index 10186038e7..40fbee1534 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt @@ -5,9 +5,9 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.token.converter.YieldBalanceConverter import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.CoroutineScope @@ -46,6 +46,8 @@ internal class DefaultYieldsBalancesStore( value = cachedStatuses.map { (stringWalletId, wrappers) -> val key = UserWalletId(stringWalletId) val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers) + .filterNotNull() + .toSet() key to value } @@ -61,7 +63,7 @@ internal class DefaultYieldsBalancesStore( override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { return runtimeStore.getSyncOrNull() ?.get(userWalletId) - ?.firstOrNull { stakingId == it.getStakingId() } + ?.firstOrNull { it.stakingId == stakingId } } override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { @@ -96,11 +98,13 @@ internal class DefaultYieldsBalancesStore( private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) + .filterNotNull() + .toSet() runtimeStore.update(default = emptyMap()) { saved -> saved.toMutableMap().apply { this[userWalletId] = saved[userWalletId] - ?.addOrReplace(newBalances) { old, new -> old.getStakingId() == new.getStakingId() } + ?.addOrReplace(newBalances) { old, new -> old.stakingId == new.stakingId } ?: newBalances } } @@ -135,7 +139,7 @@ internal class DefaultYieldsBalancesStore( val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId -> val balance = portfolioBalances - .firstOrNull { stakingId == it.getStakingId() } + .firstOrNull { it.stakingId == stakingId } ?: ifNotFound(stakingId) ?: return@mapNotNullTo null @@ -143,7 +147,7 @@ internal class DefaultYieldsBalancesStore( } val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new -> - old.getStakingId() == new.getStakingId() + old.stakingId == new.stakingId } put(key = userWalletId, value = updatedBalances) @@ -151,9 +155,7 @@ internal class DefaultYieldsBalancesStore( } } - private fun createErrorYieldBalance(id: StakingID): YieldBalance { - return YieldBalance.Error(integrationId = id.integrationId, address = id.address) - } + private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id) private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? { val integrationId = integrationId diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt index b891e42b1d..3ec260a07f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/YieldsBalancesStore.kt @@ -1,9 +1,9 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance import kotlinx.coroutines.flow.Flow /** diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt deleted file mode 100644 index 02c5f0b9a2..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.data.staking.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.blockchainsdk.utils.toMigratedCoinId -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.walletmanager.WalletManagersFacade -import javax.inject.Inject - -/** - * Factory of [StakingID] - * - * @property walletManagersFacade wallet manager facade - * -[REDACTED_AUTHOR] - */ -internal class StakingIdFactory @Inject constructor( - private val walletManagersFacade: WalletManagersFacade, -) { - - suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): StakingID? { - val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) - val integrationId = createIntegrationId(currencyId) - - if (address == null || integrationId == null) return null - - return StakingID(integrationId = integrationId, address = address) - } - - fun createIntegrationId(currencyId: CryptoCurrency.ID): String? { - val integrationKey = with(currencyId) { rawNetworkId.plus(rawCurrencyId) } - return integrationIdMap[integrationKey] - } - - fun isPolygonIntegrationId(integrationId: String): Boolean = integrationId == ETHEREUM_POLYGON_INTEGRATION_ID - - @Suppress("UnusedPrivateMember", "unused") - companion object { - - private const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" - private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" - private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" - private const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" - private const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" - private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" - private const val TRON_INTEGRATION_ID = "tron-trx-native-staking" - private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" - private const val NEAR_INTEGRATION_ID = "near-near-native-staking" - private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" - private const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking" - - // uncomment items as implementation is ready - private val integrationIdMap = mapOf( - Blockchain.TON.toDefaultKey() to TON_INTEGRATION_ID, - Blockchain.Solana.toDefaultKey() to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos.toDefaultKey() to COSMOS_INTEGRATION_ID, - Blockchain.Tron.toDefaultKey() to TRON_INTEGRATION_ID, - Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - // Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - Blockchain.BSC.toDefaultKey() to BINANCE_INTEGRATION_ID, - // Blockchain.Polkadot.toDefaultKey() to POLKADOT_INTEGRATION_ID, - // Blockchain.Avalanche.toDefaultKey() to AVALANCHE_INTEGRATION_ID, - // Blockchain.Cronos.toDefaultKey() to CRONOS_INTEGRATION_ID, - // Blockchain.Kava.toDefaultKey() to KAVA_INTEGRATION_ID, - // Blockchain.Near.toDefaultKey() to NEAR_INTEGRATION_ID, - // Blockchain.Tezos.toDefaultKey() to TEZOS_INTEGRATION_ID, - Blockchain.Cardano.toDefaultKey() to CARDANO_INTEGRATION_ID, - ) - - private fun Blockchain.toDefaultKey(): String = id + toCoinId() - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyAddressFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyAddressFactory.kt index 76fc093829..a7ea985cb6 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyAddressFactory.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyAddressFactory.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.utils import com.tangem.datasource.api.stakekit.models.request.Address -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID /** * Factory for creating [Address] diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt index 2d9c0f5581..8da94e5450 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.utils import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID /** * Factory for creating [YieldBalanceRequestBody] diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt index c04560fbeb..09b56e3b9c 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt @@ -3,8 +3,8 @@ package com.tangem.data.staking import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.converter.YieldBalanceConverter import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { - return YieldBalanceConverter(source = source).convert(this) + return YieldBalanceConverter(source = source).convert(this)!! } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt index d83b1481ee..7203f1fc73 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt @@ -1,14 +1,12 @@ package com.tangem.data.staking.multi import arrow.core.toOption -import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.test.utils.assertEitherLeft +import com.tangem.common.test.utils.assertEitherRight import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError @@ -16,8 +14,8 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -34,55 +32,46 @@ internal class DefaultMultiYieldBalanceFetcherTest { private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() - private val yieldsBalancesStore: YieldsBalancesStore = mockk() - private val stakingIdFactory: StakingIdFactory = mockk() + private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true) private val stakeKitApi: StakeKitApi = mockk() private val fetcher = DefaultMultiYieldBalanceFetcher( userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, - stakingIdFactory = stakingIdFactory, stakeKitApi = stakeKitApi, dispatchers = TestingCoroutineDispatcherProvider(), ) @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakingIdFactory, stakeKitApi) + clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakeKitApi) } @Test fun `fetch yields balances successfully`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create).sortedBy { it.integrationId } + val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create) val result = setOf( MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId), MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId), ) + coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result) - coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -91,40 +80,30 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) } - Truth.assertThat(actual.isRight()).isTrue() + assertEitherRight(actual) } @Test fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - coEvery { yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) } just Runs - val requests = listOf(YieldBalanceRequestBodyFactory.create(tonId)) val result = setOf(MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId)) coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result) - coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) @@ -132,15 +111,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } - Truth.assertThat(actual.isRight()).isTrue() + assertEitherRight(actual) } @Test fun `fetch yields balances failure if user wallet is not supported`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -149,10 +126,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher.invoke(params) // Assert - coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) @@ -162,17 +138,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if userWalletsStore returns null`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null @@ -180,10 +152,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher.invoke(params) // Assert - coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) @@ -193,69 +164,23 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) - } - - @Test - fun `fetch yields balances failure if stakingIdFactory returns null`() = runTest { - // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) - - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns null - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns null - - // Actual - val actual = fetcher.invoke(params) - - // Assert - coVerify { - userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) - } - - coVerify(inverse = true) { - yieldsBalancesStore.refresh(any(), any>()) - stakingYieldsStore.getSyncWithTimeout() - stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(any(), any()) - yieldsBalancesStore.storeError(any(), any()) - } - - val expected = IllegalStateException("Unable to create staking ids for $params: list is empty") - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -268,33 +193,23 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -307,38 +222,28 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if yields converting is failed`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf( MockYieldDTOFactory.create(tonId).copy(id = null), MockYieldDTOFactory.create(solanaId).copy(id = null), ) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -351,35 +256,25 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -394,47 +289,37 @@ internal class DefaultMultiYieldBalanceFetcherTest { """ No available yields to fetch yield balances: – userWalletId: $userWalletId - – stakingIds: ${setOf(solanaId, tonId).joinToString()} + – stakingIds: ${tonAndSolanaIds.joinToString()} """.trimIndent(), ) - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - val requests = setOf(solanaId, tonId).map(YieldBalanceRequestBodyFactory::create) + val requests = setOf(tonId, solanaId).map(YieldBalanceRequestBodyFactory::create) @Suppress("UNCHECKED_CAST") val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse> coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns errorResponse - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -445,20 +330,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = ApiResponseError.NetworkException - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } private companion object { val userWalletId = UserWalletId("011") val userWallet = MockUserWalletFactory.create() - val mocks = MockCryptoCurrencyFactory() - - val ton = mocks.createCoin(Blockchain.TON) - val solana = mocks.createCoin(Blockchain.Solana) - val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId val solanaId = StakingID( integrationId = "solana-sol-native-multivalidator-staking", diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt index 788e8c725e..8445b62e3e 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceProducerTest.kt @@ -5,9 +5,9 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.utils.getEmittedValues import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.toDomain +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt index 7bf536b73d..6722fc8d5f 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt @@ -3,8 +3,7 @@ package com.tangem.data.staking.single import arrow.core.left import arrow.core.right import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -37,15 +36,11 @@ internal class DefaultSingleYieldBalanceFetcherTest { @Test fun `fetch yield balance successfully`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = ton.id, - network = ton.network, - ) + val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) val multiParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = mapOf(ton.id to ton.network), + stakingIds = setOf(tonId), ) val multiResult = Unit.right() @@ -64,16 +59,9 @@ internal class DefaultSingleYieldBalanceFetcherTest { @Test fun `fetch yield balance failure`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = ton.id, - network = ton.network, - ) + val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) - val multiParams = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = mapOf(ton.id to ton.network), - ) + val multiParams = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId)) val multiResult = IllegalStateException().left() @@ -89,6 +77,6 @@ internal class DefaultSingleYieldBalanceFetcherTest { private companion object { val userWalletId = UserWalletId("011") - val ton = MockCryptoCurrencyFactory().createCoin(Blockchain.TON) + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId } } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt index 1d7e5af029..cef37862e0 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -1,54 +1,60 @@ package com.tangem.data.staking.single import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.getEmittedValues import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.data.staking.toDomain -import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultSingleYieldBalanceProducerTest { private val params = SingleYieldBalanceProducer.Params( userWalletId = UserWalletId(stringValue = "011"), - currencyId = ton.id, - network = ton.network, + stakingId = tonId, ) private val multiNetworkStatusSupplier = mockk() - private val stakingIdFactory = mockk() private val analyticsExceptionHandler = mockk(relaxUnitFun = true) private val dispatchers = TestingCoroutineDispatcherProvider() private val producer = DefaultSingleYieldBalanceProducer( params = params, - stakingIdFactory = stakingIdFactory, multiYieldBalanceSupplier = multiNetworkStatusSupplier, analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = dispatchers, ) + @BeforeEach + fun resetMocks() { + clearMocks(multiNetworkStatusSupplier, analyticsExceptionHandler) + } + @Test - fun `test that flow is mapped for data from params`() = runTest { + fun `flow is mapped for data from params`() = runTest { + // Arrange val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - val expected = flowOf( + val multiFlow = flowOf( setOf( balance, MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), @@ -56,97 +62,89 @@ internal class DefaultSingleYieldBalanceProducerTest { ) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produce() + // Act + val actual = getEmittedValues(flow = producer.produce()) - verify { multiNetworkStatusSupplier(multiParams) } + Truth.assertThat(actual).hasSize(1) + Truth.assertThat(actual).containsExactly(balance) - val values = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(balance)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test that flow is updated if yield balance is updated`() = runTest { - val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + fun `flow is updated if yield balance is updated`() = runTest { + // Arrange + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produceWithFallback() + val producerFlow = producer.produceWithFallback() - verify { multiNetworkStatusSupplier(multiParams) } - - // first emit val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - expected.emit(value = setOf(balance)) + val updatedBalance = YieldBalance.Error(stakingId = tonId) - val values1 = getEmittedValues(flow = actual) + // Act (first emit) + multiFlow.emit(value = setOf(balance)) + val actual1 = getEmittedValues(flow = producerFlow) - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + // Assert (first emit) + Truth.assertThat(actual1).hasSize(1) + Truth.assertThat(actual1).containsExactly(balance) - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1).isEqualTo(listOf(balance)) + // Act (second emit) + multiFlow.emit(value = setOf(updatedBalance)) + val actual2 = getEmittedValues(flow = producerFlow) - // second emit - val updatedStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = tonId.address) - expected.emit(value = setOf(updatedStatus)) + // Assert (second emit) + Truth.assertThat(actual2).hasSize(2) + Truth.assertThat(actual2).containsExactly(balance, updatedBalance) - val values2 = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values2.size).isEqualTo(2) - Truth.assertThat(values2).isEqualTo(listOf(balance, updatedStatus)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test that flow is filtered the same status`() = runTest { - val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + fun `flow is filtered the same status`() = runTest { + // Arrange + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produceWithFallback() + val producerFlow = producer.produceWithFallback() - verify { multiNetworkStatusSupplier(multiParams) } - - // first emit val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - expected.emit(value = setOf(balance)) - val values1 = getEmittedValues(flow = actual) + // Act (first emit) + multiFlow.emit(value = setOf(balance)) + val actual1 = getEmittedValues(flow = producerFlow) - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + // Assert (first emit) + Truth.assertThat(actual1).hasSize(1) + Truth.assertThat(actual1).containsExactly(balance) - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1).isEqualTo(listOf(balance)) + // Act (second emit) + multiFlow.emit(value = setOf(balance)) + val actual2 = getEmittedValues(flow = producerFlow) - // second emit - expected.emit(value = setOf(balance)) + // Assert (second emit) + Truth.assertThat(actual2).hasSize(1) + Truth.assertThat(actual2).containsExactly(balance) - val values2 = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values2.size).isEqualTo(1) - Truth.assertThat(values2).isEqualTo(listOf(balance)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test if flow throws exception`() = runTest { + fun `flow throws exception`() = runTest { + // Arrange val exception = IllegalStateException() + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() val innerFlow = MutableStateFlow(value = false) - val expected = flow { + val multiFlow = flow { if (innerFlow.value) { emit(setOf(balance)) } else { @@ -156,83 +154,52 @@ internal class DefaultSingleYieldBalanceProducerTest { .buffer(capacity = 5) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - every { stakingIdFactory.createIntegrationId(currencyId = params.currencyId) } returns tonId.integrationId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produceWithFallback() + val producerFlow = producer.produceWithFallback() - verify { multiNetworkStatusSupplier(multiParams) } + // Act (first emit) + val actual1 = getEmittedValues(flow = producerFlow) - val values1 = getEmittedValues(flow = actual) + // Assert (first emit) + val fallbackStatus = YieldBalance.Error(stakingId = tonId.copy(address = "0x1")) - coVerify(inverse = true) { stakingIdFactory.create(any(), any(), any()) } - - Truth.assertThat(values1.size).isEqualTo(1) - val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = null) - Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) - - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + Truth.assertThat(actual1).hasSize(1) + Truth.assertThat(actual1).containsExactly(fallbackStatus) + // Act (second emit) innerFlow.emit(value = true) + val actual2 = getEmittedValues(flow = producerFlow) - val values2 = getEmittedValues(flow = actual) + Truth.assertThat(actual2).hasSize(1) + Truth.assertThat(actual2).containsExactly(balance) - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values2.size).isEqualTo(1) - Truth.assertThat(values2).isEqualTo(listOf(balance)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test if flow doesn't contain network from params`() = runTest { + fun `flow doesn't contain network from params`() = runTest { + // Arrange val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain() - val yieldBalancesFlow = flowOf(setOf(balance)) + val multiFlow = flowOf(setOf(balance)) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produce() + val producerFlow = producer.produce() - verify { multiNetworkStatusSupplier(multiParams) } + // Act + val actual = getEmittedValues(flow = producerFlow) - val values = getEmittedValues(flow = actual) + // Assert + Truth.assertThat(actual).isEmpty() - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - val expected = YieldBalance.Unsupported - Truth.assertThat(values.first()).isEqualTo(expected) - } - - @Test - fun `test if wallet manager facade returns null`() = runTest { - val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - - val yieldBalancesFlow = flowOf(setOf(balance)) - - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns null - - val actual = producer.produce() - - verify { multiNetworkStatusSupplier(multiParams) } - - val values = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - val expected = YieldBalance.Unsupported - Truth.assertThat(values.first()).isEqualTo(expected) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } private companion object { - val mocks = MockCryptoCurrencyFactory() - - val ton = mocks.createCoin(Blockchain.TON) - val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId val solanaId = StakingID( integrationId = "solana-sol-native-multivalidator-staking", diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt index 2fb8e91a50..8227216a40 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreGetMethodTest.kt @@ -6,8 +6,8 @@ import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.common.test.utils.getEmittedValues import com.tangem.data.staking.toDomain import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt index 2cd4f2efb7..8885ab72bc 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreInitializationTest.kt @@ -6,8 +6,8 @@ import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.datastore.MockStateDataStore import com.tangem.data.staking.toDomain import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt index 9d80251ee9..febababb02 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt @@ -7,9 +7,9 @@ import com.tangem.data.staking.toDomain import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.test.runTest @@ -129,12 +129,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId)) val runtimeExpected = mapOf( - userWalletId to setOf( - YieldBalance.Error( - integrationId = stakingId.integrationId, - address = stakingId.address, - ), - ), + userWalletId to setOf(YieldBalance.Error(stakingId)), ) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt deleted file mode 100644 index 9244ac2684..0000000000 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.tangem.data.staking.utils - -import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.common.test.utils.ProvideTestModels -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.walletmanager.WalletManagersFacade -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.params.ParameterizedTest - -/** -[REDACTED_AUTHOR] - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class StakingIdFactoryTest { - - private val walletManagersFacade: WalletManagersFacade = mockk() - private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade) - - @BeforeEach - fun resetMocks() { - clearMocks(walletManagersFacade) - } - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class CreateIntegrationId { - - @ParameterizedTest - @ProvideTestModels - fun createIntegrationId(model: CreateIntegrationIdModel) { - // Act - val actual = factory.createIntegrationId(currencyId = model.currencyId) - - // Assert - Truth.assertThat(actual).isEqualTo(model.expected) - } - - private fun provideTestModels() = listOf( - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.TON), - expected = "ton-ton-chorus-one-pools-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Solana), - expected = "solana-sol-native-multivalidator-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), - expected = "cosmos-atom-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Tron), - expected = "tron-trx-native-staking", - ), - CreateIntegrationIdModel( - currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-tokenāš“"), - expected = "ethereum-matic-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.BSC), - expected = "bsc-bnb-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cardano), - expected = "cardano-ada-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin), - expected = null, - ), - ) - } - - data class CreateIntegrationIdModel(val currencyId: CryptoCurrency.ID, val expected: String?) - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class Create { - - private val defaultAddress = "address" - - @Test - fun `create returns null if address is null`() = runTest { - // Arrange - val userWalletId = UserWalletId(stringValue = "011") - val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON) - - coEvery { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network) - } returns null - - // Act - val actual = factory.create( - userWalletId = userWalletId, - currencyId = currency.id, - network = currency.network, - ) - - // Assert - val expected = null - Truth.assertThat(actual).isEqualTo(expected) - - coVerify(exactly = 1) { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network) - } - } - - @ParameterizedTest - @ProvideTestModels - fun create(model: CreateModel) = runTest { - // Arrange - val userWalletId = UserWalletId(stringValue = "011") - val network = MockCryptoCurrencyFactory().createCoin(Blockchain.TON).network - - coEvery { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) - } returns defaultAddress - - // Act - val actual = factory.create(userWalletId = userWalletId, currencyId = model.currencyId, network = network) - - // Assert - Truth.assertThat(actual).isEqualTo(model.expected) - - coVerify(exactly = 1) { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) - } - } - - private fun provideTestModels() = listOf( - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.TON), - expected = createStakingId(integrationId = "ton-ton-chorus-one-pools-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Solana), - expected = createStakingId(integrationId = "solana-sol-native-multivalidator-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), - expected = createStakingId(integrationId = "cosmos-atom-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Tron), - expected = createStakingId(integrationId = "tron-trx-native-staking"), - ), - CreateModel( - currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-tokenāš“"), - expected = createStakingId(integrationId = "ethereum-matic-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.BSC), - expected = createStakingId(integrationId = "bsc-bnb-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cardano), - expected = createStakingId(integrationId = "cardano-ada-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin), - expected = null, - ), - ) - - private fun createStakingId(integrationId: String): StakingID { - return StakingID(integrationId = integrationId, address = defaultAddress) - } - } - - data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingID?) - - private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID { - return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩${blockchain.toCoinId()}āš“") - } -} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 51f04d6a97..82aa29d68a 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -22,14 +22,15 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -65,13 +66,15 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, - ): List = withContext(coroutineDispatcher.io) { + swapTxType: SwapTxType, + ): List = withContext(coroutineDispatcher.default) { val cryptoCurrencyList = cryptoCurrencyStatusList.map { it.currency } val allPairs = getPairsInternal( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyList = cryptoCurrencyList, + swapTxType = swapTxType, ) val providers = expressRepository.getProviders( @@ -113,11 +116,13 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, - ): List = withContext(coroutineDispatcher.io) { + swapTxType: SwapTxType, + ): List = withContext(coroutineDispatcher.default) { val allPairs = getPairsInternal( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyList = cryptoCurrencyList, + swapTxType = swapTxType, ) val providers = expressRepository.getProviders( @@ -307,48 +312,60 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet: UserWallet, initialCurrency: CryptoCurrency, cryptoCurrencyList: List, - ) = awaitAll( - // original pairs - async { + swapTxType: SwapTxType, + ) = when (swapTxType) { + SwapTxType.Swap -> awaitAll( + // original pairs + async { + invokePairRequest( + userWallet = userWallet, + from = arrayListOf(initialCurrency), + to = cryptoCurrencyList, + ) + }, + // reversed pairs + async { + invokePairRequest( + userWallet = userWallet, + from = cryptoCurrencyList, + to = arrayListOf(initialCurrency), + ) + }, + ).flatten() + SwapTxType.SendWithSwap -> { invokePairRequest( userWallet = userWallet, from = arrayListOf(initialCurrency), to = cryptoCurrencyList, ) - }, - // reversed pairs - async { - invokePairRequest( - userWallet = userWallet, - from = cryptoCurrencyList, - to = arrayListOf(initialCurrency), - ) - }, - ).flatten() + } + } private suspend fun invokePairRequest( userWallet: UserWallet, from: List, to: List, - ) = safeApiCall( - call = { - tangemExpressApi.getPairs( - userWalletId = userWallet.walletId.stringValue, - refCode = ExpressUtils.getRefCode( - userWallet = userWallet, - appPreferencesStore = appPreferencesStore, - ), - body = PairsRequestBody( - from = tokenInfoConverter.convertList(from), - to = tokenInfoConverter.convertList(to), - ), - ).getOrThrow() - }, - onError = { - Timber.w(it, "Unable to get pairs") - throw it - }, - ) + ) = withContext(coroutineDispatcher.io) { + safeApiCall( + call = { + tangemExpressApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = PairsRequestBody( + from = tokenInfoConverter.convertList(from), + to = tokenInfoConverter.convertList(to), + ), + ).getOrThrow() + }, + onError = { + Timber.w(it, "Unable to get pairs") + throw it + }, + ) + } /** * Send with swap specific currency status creation @@ -360,9 +377,9 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val quote = singleQuoteStatusSupplier.getSyncOrNull( params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId), - )?.right() + ) - if (quote == null) { + if (quote == null || quote.value is QuoteStatus.Empty) { singleQuoteStatusFetcher.invoke( params = SingleQuoteStatusFetcher.Params( rawCurrencyId = rawCurrencyId, @@ -373,7 +390,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( return currencyStatusProxyCreator.createCurrencyStatus( currency = cryptoCurrency, - maybeQuoteStatus = quote ?: singleQuoteStatusSupplier.getSyncOrNull( + maybeQuoteStatus = quote?.right() ?: singleQuoteStatusSupplier.getSyncOrNull( params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId), ).right(), maybeNetworkStatus = NetworkStatus( diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index c2d3527060..072cf92212 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -13,7 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync -import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -24,9 +24,8 @@ import com.tangem.domain.swap.models.SwapTransactionModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, @@ -95,36 +94,34 @@ internal class DefaultSwapTransactionRepository( } } - override suspend fun getTransactions( + override fun getTransactions( userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, - ): Flow?> { - return withContext(dispatchers.io) { - val txStatuses = appPreferencesStore.getObjectMapSync( - key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, - ) - appPreferencesStore.getObjectList( - key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, - ).map { savedTransactions -> - val currencyTxs = savedTransactions - ?.filter { - it.userWalletId == userWallet.walletId.stringValue && - ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value - ) - } + ): Flow?> = combine( + flow = appPreferencesStore.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ), + flow2 = appPreferencesStore.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ), + ) { savedTransactions, txStatuses -> + val currencyTxs = savedTransactions + ?.filter { + it.userWalletId == userWallet.walletId.stringValue && + ( + it.toCryptoCurrencyId == cryptoCurrencyId.value || + it.fromCryptoCurrencyId == cryptoCurrencyId.value + ) + } - currencyTxs?.mapNotNull { - listConverter.convertBack( - value = it, - userWallet = userWallet, - txStatuses = txStatuses, - ) - } - }.flowOn(dispatchers.io) + currencyTxs?.mapNotNull { + listConverter.convertBack( + value = it, + userWallet = userWallet, + txStatuses = txStatuses, + ) } - } + }.flowOn(dispatchers.default) override suspend fun removeTransaction( userWalletId: UserWalletId, @@ -188,7 +185,7 @@ internal class DefaultSwapTransactionRepository( ) val updatesMap = savedMap.toMutableMap() - updatesMap[txId] = savedStatusConverter.convertBack( + updatesMap[txId] = savedStatusConverter.convert( status.copy( refundTokensResponse = refundTokenCurrency?.let { userTokensResponseFactory.createResponseToken(refundTokenCurrency) diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt index c3d8f36903..20c6016104 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt @@ -6,9 +6,9 @@ import com.tangem.domain.swap.models.SwapStatus import com.tangem.domain.swap.models.SwapStatusModel import com.tangem.utils.converter.TwoWayConverter -internal class SavedSwapStatusConverter : TwoWayConverter { +internal class SavedSwapStatusConverter : TwoWayConverter { - override fun convert(value: SwapStatusDTO) = SwapStatusModel( + override fun convertBack(value: SwapStatusDTO) = SwapStatusModel( providerId = value.providerId, status = SwapStatus.entries.firstOrNull { it.name.lowercase() == value.status?.name?.lowercase() @@ -22,7 +22,7 @@ internal class SavedSwapStatusConverter : TwoWayConverter> { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId).requireColdWallet() // TODO [REDACTED_TASK_KEY] + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) if (!userWallet.isMultiCurrency) { error("${this::class.simpleName} supports only multi-currency wallet") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 6687fab2f7..190429d257 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -22,9 +22,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index e9c3fdce89..f374404cbe 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -7,11 +7,11 @@ import com.tangem.blockchain.common.ReserveAmountProvider import com.tangem.blockchain.common.UtxoAmountLimitProvider import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.utils.getTotalStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 05af092bd5..38d1d2957a 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { /** Project - Domain */ implementation(projects.domain.visa) implementation(projects.domain.card) + implementation(projects.domain.wallets) implementation(projects.domain.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) @@ -49,6 +50,7 @@ dependencies { /** Libs - Tangem */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(projects.libs.tangemSdkApi) /** DI */ implementation(deps.hilt.core) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt index e42238404f..cc1a69895e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt @@ -2,39 +2,52 @@ package com.tangem.data.pay import arrow.core.Either import com.squareup.moshi.Moshi +import com.tangem.common.map import com.tangem.core.error.UniversalError import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.KycStartInfo import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.sdk.api.TangemSdkManager import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.withContext -@Suppress("UnusedPrivateMember") class DefaultKycRepository @AssistedInject constructor( - @Assisted userWalletId: UserWalletId, @NetworkMoshi moshi: Moshi, private val tangemPayApi: TangemPayApi, - private val dispatcherProvider: CoroutineDispatcherProvider, + private val visaAuthRepository: VisaAuthRepository, + private val tangemSdkManager: TangemSdkManager, ) : KycRepository { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) - override suspend fun getKycStartInfo(): Either = withContext(dispatcherProvider.io) { - val authTokenForSpecificWallet = "get from userWalletId" - - request { - tangemPayApi.getKycAccess( - authHeader = authTokenForSpecificWallet, - ).getOrThrow().result + override suspend fun getKycStartInfo(address: String, cardId: String): Either { + var authHeader = "" + visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result -> + tangemSdkManager.visaCustomerWalletApprove( + VisaDataForApprove( + customerWalletCardId = cardId, + targetAddress = address, + dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge), + ), + ).map { signResult -> + visaAuthRepository.getTokenWithCustomerWallet( + sessionId = result.session.sessionId, + signature = signResult.signature, + nonce = signResult.dataToSign.hashToSign, + ).getOrNull()?.let { authHeader = it } + } + } + return request { + authHeader.ifEmpty { error("Cannot get auth header for KYC") } + tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result }.map { KycStartInfo( token = it.token, @@ -65,6 +78,6 @@ class DefaultKycRepository @AssistedInject constructor( @AssistedFactory interface Factory : KycRepository.Factory { - override fun create(userWalletId: UserWalletId): DefaultKycRepository + override fun create(): DefaultKycRepository } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 30457ea047..b38b9a82a8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -131,16 +131,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - visaApi.activateByCustomerWallet( - authHeader = authTokens.getAuthHeader(), - body = ActivationByCustomerWalletRequest( - orderId = signedData.dataToSign.request.orderId, - customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( - deployAcceptanceSignature = signedData.signature, - customerWalletAddress = signedData.customerWalletAddress, + signedData.dataToSign.request?.orderId?.let { orderId -> + visaApi.activateByCustomerWallet( + authHeader = authTokens.getAuthHeader(), + body = ActivationByCustomerWalletRequest( + orderId = orderId, + customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( + deployAcceptanceSignature = signedData.signature, + customerWalletAddress = signedData.customerWalletAddress, + ), ), - ), - ).getOrThrow() + ).getOrThrow() + } ?: error("Order Id cannot be null") } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index c9c07624e5..8b3024f1fe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -65,6 +65,39 @@ internal class DefaultVisaAuthRepository @Inject constructor( } } + override suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.generateNonceByCustomerWallet( + GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress), + ).getOrThrow() + }.map { response -> + VisaAuthChallenge.Wallet( + challenge = response.result.nonce, + session = VisaAuthSession(response.result.sessionId), + ) + } + } + + override suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.getTokenByCustomerWallet( + GetTokenByCustomerWalletRequest( + sessionId = sessionId, + signature = signature, + messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + ), + ).getOrThrow() + }.map { response -> + "Bearer ${response.result.accessToken}" + } + } + override suspend fun getAccessTokens( signedChallenge: VisaAuthSignedChallenge, ): Either = withContext(dispatchers.io) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index f4a46dab61..829d185c39 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -3,12 +3,11 @@ package com.tangem.data.visa.utils import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory -import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.visa.model.VisaContractInfo import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -34,7 +33,7 @@ internal class VisaCurrencyFactory @Inject constructor( val currencyNetwork = networkFactory.create( blockchain = Blockchain.Polygon, extraDerivationPath = null, - derivationStyleProvider = userWallet.requireColdWallet().scanResponse.derivationStyleProvider, + derivationStyleProvider = userWallet.derivationStyleProvider, canHandleTokens = true, ) ?: error("Unable to create network for Visa currency") diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index 01e9213ddc..edd6da8af5 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,10 +7,11 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp -import com.tangem.domain.card.DerivationStyleProvider -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber internal class WalletManagerFactory( @@ -41,7 +42,7 @@ internal class WalletManagerFactory( blockchain: Blockchain, derivationPath: DerivationPath?, ): WalletManager? { - val curve = blockchain.getSupportedCurves().first() + val curve = hotWallet.curvesConfig.primaryCurve(blockchain) val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } ?: return null return try { diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index fc8569a999..f7867f6b5d 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -11,9 +11,14 @@ android { } dependencies { + implementation(projects.data.common) /** Tangem libraries */ - implementation(tangemDeps.blockchain) // android-library + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) + implementation(projects.libs.tangemSdkApi) + implementation(projects.libs.blockchainSdk) /** Core */ implementation(projects.core.datasource) @@ -21,6 +26,7 @@ dependencies { /** Domain */ implementation(projects.domain.wallets) + implementation(projects.domain.card) api(projects.domain.models) /** Domain models */ @@ -29,15 +35,17 @@ dependencies { /** DI */ implementation(deps.hilt.android) - implementation(project(":domain:legacy")) kapt(deps.hilt.kapt) /** Other deps */ implementation(deps.androidx.datastore) implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.timber) /** tests */ testImplementation(projects.domain.models) + testImplementation(projects.common.test) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 8808bd336a..2127d9b80b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -17,6 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFI import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.wallet.UserWallet @@ -47,14 +48,78 @@ internal class DefaultWalletsRepository( return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override fun shouldSaveUserWallets(): Flow { return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override suspend fun saveShouldSaveUserWallets(item: Boolean) { appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item) } + override suspend fun useBiometricAuthentication(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + ) + + if (useBiometricAuthentication != null) { + return useBiometricAuthentication + } + + val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SAVE_USER_WALLETS_KEY, + ) + + if (legacySaveWalletsInTheApp != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + value = legacySaveWalletsInTheApp, + ) + return legacySaveWalletsInTheApp + } else { + // Default value for new users + setUseBiometricAuthentication(false) + return false + } + } + + override suspend fun setUseBiometricAuthentication(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, value = value) + } + + override suspend fun requireAccessCode(): Boolean { + val requireAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + ) + + if (requireAccessCode != null) { + return requireAccessCode + } + + val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, + ) + + if (legacyShouldSaveAccessCode != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + value = legacyShouldSaveAccessCode.not(), + ) + return legacyShouldSaveAccessCode.not() + } else { + // Default value for new users + setRequireAccessCode(true) + return true + } + } + + override suspend fun setRequireAccessCode(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value) + } + override suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean { return appPreferencesStore .getSyncOrDefault(key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt similarity index 56% rename from app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt index b09a27d4cd..e00bde14e5 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt @@ -1,51 +1,50 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.cold 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 -import com.tangem.common.doOnFailure -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.network.NetworkFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.card.BackendId -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.data.wallets.derivations.Derivations +import com.tangem.data.wallets.derivations.MissedDerivationsFinder import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.sdk.api.TangemSdkManager -import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber +import javax.inject.Inject -internal typealias Derivations = Map> private typealias DerivedKeys = Map -internal class DefaultDerivationsRepository( +internal class DefaultColdMapDerivationsRepository @Inject constructor( private val tangemSdkManager: TangemSdkManager, - private val userWalletsStore: UserWalletsStore, private val networkFactory: NetworkFactory, private val dispatchers: CoroutineDispatcherProvider, -) : DerivationsRepository { +) : ColdMapDerivationsRepository { - override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { - derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) + override suspend fun derivePublicKeys( + userWallet: UserWallet.Cold, + currencies: List, + ): UserWallet.Cold = withContext(dispatchers.io) { + derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network)) } - override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") - + override suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Cold, + networkIds: List, + ): UserWallet.Cold = withContext(dispatchers.io) { derivePublicKeysByNetworks( - userWalletId = userWalletId, + userWallet = userWallet, networks = networkIds.mapNotNull { networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, @@ -56,44 +55,55 @@ internal class DefaultDerivationsRepository( ) } - override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { - val userWallet = withContext(dispatchers.io) { - userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") - } - - if (userWallet is UserWallet.Hot) { - return - } - - userWallet.requireColdWallet() - + override suspend fun derivePublicKeysByNetworks( + userWallet: UserWallet.Cold, + networks: List, + ): UserWallet.Cold = withContext(dispatchers.io) { if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { Timber.d("Nothing to derive") - return + return@withContext userWallet } - val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) + val derivations = MissedDerivationsFinder(userWallet) .findByNetworks(networks) .ifEmpty { Timber.d("Nothing to derive") - return + return@withContext userWallet } - derivePublicKeys(userWalletId = userWalletId, derivations = derivations) + return@withContext derivePublicKeys(userWallet = userWallet, derivations = derivations).first + } + + override suspend fun derivePublicKeys( + userWallet: UserWallet.Cold, + derivations: Map>, + ): Pair> = withContext(dispatchers.io) { + // todo replace it in task [REDACTED_JIRA] + val preflightReadFilter = UserWalletIdPreflightReadFilter(userWallet.walletId) + val result = tangemSdkManager.derivePublicKeys( + cardId = null, + derivations = derivations, + preflightReadFilter = preflightReadFilter, + ) + + when (result) { + is CompletionResult.Success -> { + userWallet.updateDerivedKeys(result.data.entries).also { + validateDerivations(scanResponse = it.scanResponse, derivations = derivations) + } to result.data.entries + } + is CompletionResult.Failure -> { + throw result.error + } + } } override suspend fun hasMissedDerivations( - userWalletId: UserWalletId, + userWallet: UserWallet.Cold, networksWithDerivationPath: Map, - ): Boolean { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") - - if (userWallet is UserWallet.Hot) { - return false - } - + ): Boolean = withContext(dispatchers.io) { val derivations = - MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) + MissedDerivationsFinder(userWallet) .findByNetworks( networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) -> networkFactory.create( @@ -104,28 +114,7 @@ internal class DefaultDerivationsRepository( }, ) - return derivations.isNotEmpty() - } - - override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys { - // todo replace it in task [REDACTED_JIRA] - val preflightReadFilter = UserWalletIdPreflightReadFilter(userWalletId) - tangemSdkManager.derivePublicKeys( - cardId = null, - derivations = derivations, - preflightReadFilter = preflightReadFilter, - ).doOnSuccess { response -> - updatePublicKeys(userWalletId = userWalletId, keys = response.entries) - .doOnSuccess { - // TODO [REDACTED_TASK_KEY] - validateDerivations(scanResponse = it.requireColdWallet().scanResponse, derivations = derivations) - return response.entries - } - .doOnFailure { throw it } - } - .doOnFailure { throw it } - - error("This code should never be reached") + derivations.isNotEmpty() } /** @@ -144,16 +133,7 @@ internal class DefaultDerivationsRepository( } } - private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult { - return withContext(dispatchers.io) { - userWalletsStore.update( - userWalletId = userWalletId, - update = { userWallet -> userWallet.requireColdWallet().updateDerivedKeys(keys) }, // TODO [REDACTED_TASK_KEY] - ) - } - } - - private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet { + private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet.Cold { return copy( scanResponse = scanResponse.copy( derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys), diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt b/data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt new file mode 100644 index 0000000000..dcb1dc574b --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt @@ -0,0 +1,25 @@ +package com.tangem.data.wallets.cold + +import com.tangem.common.card.Card +import com.tangem.common.core.SessionEnvironment +import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.operations.preflightread.PreflightReadFilter + +/** + * [PreflightReadFilter] for checking if card has expected user wallet id + * +[REDACTED_AUTHOR] + */ +class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWalletId) : PreflightReadFilter { + + override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit + + override fun onFullCardRead(card: Card, environment: SessionEnvironment) { + val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return + + if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound() + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt new file mode 100644 index 0000000000..862b90b035 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -0,0 +1,95 @@ +package com.tangem.data.wallets.derivations + +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.map +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultDerivationsRepository @Inject constructor( + private val userWalletsStore: UserWalletsStore, + private val hotDerivationsRepository: HotMapDerivationsRepository, + private val coldDerivationsRepository: ColdMapDerivationsRepository, + private val dispatchers: CoroutineDispatcherProvider, +) : DerivationsRepository { + + override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { + derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) + } + + override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + when (userWallet) { + is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) + is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) + }.also { + userWallet.update(it) + } + } + + override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + when (userWallet) { + is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) + is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) + }.also { + userWallet.update(it) + } + } + + override suspend fun derivePublicKeys( + userWalletId: UserWalletId, + derivations: Map>, + ): Map { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + return when (userWallet) { + is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations) + is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations) + }.let { + userWallet.update(it.first) + it.second + } + } + + override suspend fun hasMissedDerivations( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean { + return when (val userWallet = userWalletsStore.getSyncStrict(userWalletId)) { + is UserWallet.Cold -> coldDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) + is UserWallet.Hot -> hotDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) + } + } + + private suspend fun UserWallet.update(newUserWallet: UserWallet) = withContext(dispatchers.io) { + check(this@update.walletId == newUserWallet.walletId) { + "Cannot update UserWallet with different walletId: ${newUserWallet.walletId}" + } + + if (this@update == newUserWallet) { + return@withContext // No update needed + } + + val updateResult = userWalletsStore.update( + userWalletId = newUserWallet.walletId, + update = { userWalletToUpdate -> newUserWallet }, + ) + + when (updateResult) { + is CompletionResult.Failure -> throw updateResult.error + is CompletionResult.Success -> updateResult.data + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt similarity index 64% rename from app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index 7e740159e5..ce12ab7516 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain @@ -7,24 +7,26 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.operations.derivation.ExtendedPublicKeysMap +import kotlin.collections.forEach private typealias DerivationData = Pair> +internal typealias Derivations = Map> /** * Finder of missed derivations * - * @property scanResponse scanning response + * @property userWallet User wallet to find derivations for * [REDACTED_AUTHOR] */ -internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { +internal class MissedDerivationsFinder(private val userWallet: UserWallet) { /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { @@ -48,30 +50,35 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } private fun List.mapToNewDerivations(): List { - val config = CardConfig.createConfig(scanResponse.card) return mapNotNull { network -> val blockchain = network.toBlockchain() - val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null + val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null - findNewDerivations(curve = curve, scanResponse = scanResponse, network = network) + val walletPublicKey = when (userWallet) { + is UserWallet.Cold -> { + val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve } + wallet?.publicKey + } + is UserWallet.Hot -> { + val wallet = userWallet.wallets?.firstOrNull { it.curve == curve } + wallet?.publicKey + } + } + + walletPublicKey?.let { + findNewDerivations(curve = curve, publicKey = it, network = network) + } } } - private fun findNewDerivations( - curve: EllipticCurve, - scanResponse: ScanResponse, - network: Network, - ): DerivationData? { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - val publicKey = wallet.publicKey.toMapKey() - + private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? { val derivationCandidates = network .getDerivationCandidates(curve) .ifEmpty { return null } - .filterAlreadyDerivedKeys(publicKey) + .filterAlreadyDerivedKeys(publicKey.toMapKey()) .ifEmpty { return null } - return publicKey to derivationCandidates + return publicKey.toMapKey() to derivationCandidates } private fun Network.getDerivationCandidates(curve: EllipticCurve): List { @@ -88,7 +95,7 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { return if (getSupportedCurves().contains(curve)) { - derivationPath(style = scanResponse.derivationStyleProvider.getDerivationStyle()) + derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle()) } else { null } @@ -118,7 +125,15 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val extendedPublicKeysMap = scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val extendedPublicKeysMap = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) + is UserWallet.Hot -> { + val wallets = userWallet.wallets ?: return emptyList() + wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys + ?: ExtendedPublicKeysMap(emptyMap()) + } + } + return extendedPublicKeysMap.keys.toList() } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index a154dc4f99..bcaff10b6f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -2,14 +2,23 @@ package com.tangem.data.wallets.di import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository +import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository +import com.tangem.data.wallets.derivations.DefaultDerivationsRepository +import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository +import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -44,4 +53,27 @@ internal object WalletsDataModule { fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository { return DefaultWalletNamesMigrationRepository(appPreferencesStore) } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletsDataBindsModule { + + @Binds + @Singleton + fun bindDerivationsRepository(impl: DefaultDerivationsRepository): DerivationsRepository + + @Binds + @Singleton + fun bindHotMapDerivationsRepository(impl: DefaultHotMapDerivationsRepository): HotMapDerivationsRepository + + @Binds + @Singleton + fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository + + @Binds + @Singleton + fun bindHotWalletAccessCodeAttemptsRepository( + impl: DefaultHotWalletAccessCodeAttemptsRepository, + ): HotWalletAccessCodeAttemptsRepository } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt new file mode 100644 index 0000000000..3eb9431fc3 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -0,0 +1,139 @@ +package com.tangem.data.wallets.hot + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.wallets.derivations.MissedDerivationsFinder +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.hot.sdk.model.DeriveWalletRequest +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +internal class DefaultHotMapDerivationsRepository @Inject constructor( + private val networkFactory: NetworkFactory, + private val hotWalletAccessor: HotWalletAccessor, + private val dispatchers: CoroutineDispatcherProvider, +) : HotMapDerivationsRepository { + + override suspend fun derivePublicKeys( + userWallet: UserWallet.Hot, + currencies: List, + ): UserWallet.Hot { + return derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network)) + } + + override suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Hot, + networkIds: List, + ): UserWallet.Hot { + return derivePublicKeysByNetworks( + userWallet = userWallet, + networks = networkIds.mapNotNull { + networkFactory.create( + blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, + extraDerivationPath = null, + userWallet = userWallet, + ) + }, + ) + } + + override suspend fun derivePublicKeysByNetworks( + userWallet: UserWallet.Hot, + networks: List, + ): UserWallet.Hot = withContext(dispatchers.default) { + val derivations = MissedDerivationsFinder(userWallet) + .findByNetworks(networks) + .ifEmpty { + Timber.d("Nothing to derive") + return@withContext userWallet + } + + derivePublicKeys(userWallet, derivations).first + } + + override suspend fun derivePublicKeys( + userWallet: UserWallet.Hot, + derivations: Map>, + ): Pair> { + val wallets = userWallet.wallets ?: return userWallet to emptyMap() + + val request = DeriveWalletRequest( + derivations.map { entry -> + val wallet = wallets.first { it.publicKey.contentEquals(entry.key.bytes) } + DeriveWalletRequest.Request( + curve = wallet.curve, + paths = entry.value, + ) + }, + ) + val result = hotWalletAccessor.derivePublicKeys( + hotWalletId = userWallet.hotWalletId, + request = request, + ) + val newKeys = + result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) } + + return userWallet.updateWithNewKeys(newKeys) to newKeys + } + + override suspend fun hasMissedDerivations( + userWallet: UserWallet.Hot, + networksWithDerivationPath: Map, + ): Boolean = withContext(dispatchers.default) { + val derivations = MissedDerivationsFinder(userWallet) + .findByNetworks( + networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) -> + networkFactory.create( + blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null, + extraDerivationPath = extraDerivationPath, + userWallet = userWallet, + ) + }, + ) + + derivations.isNotEmpty() + } + + private fun UserWallet.Hot.updateWithNewKeys(newKeys: Map): UserWallet.Hot { + val wallets = this.wallets ?: return this + val derivedKeys = wallets.associate { + it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys) + } + val updatedKeys = getUpdatedDerivedKeys( + oldKeys = derivedKeys, + newKeys = newKeys, + ) + + return copy( + wallets = wallets.map { wallet -> + wallet.copy( + derivedKeys = updatedKeys[wallet.publicKey.toMapKey()] ?: ExtendedPublicKeysMap(emptyMap()), + ) + }, + ) + } + + private fun getUpdatedDerivedKeys( + oldKeys: Map, + newKeys: Map, + ): Map { + return (oldKeys.keys + newKeys.keys).toSet() + .associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap()) + val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..8a77f2f70a --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,138 @@ +package com.tangem.data.wallets.hot + +import android.content.Context +import android.os.SystemClock +import android.provider.Settings +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.COOLDOWN_SECONDS +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS +import com.tangem.hot.sdk.model.HotWalletId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@Suppress("MagicNumber") +class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val appPreferencesStore: AppPreferencesStore, +) : HotWalletAccessCodeAttemptsRepository { + + override suspend fun incrementAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + val attemptsKey = PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey()) + + appPreferencesStore.editData { preferences -> + val currentAttempts = preferences[attemptsKey] ?: 0 + val newAttempts = currentAttempts + 1 + + preferences[attemptsKey] = newAttempts + val currentBootCount = currentBootCount() + preferences[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] = currentBootCount + + if (newAttempts >= MAX_FAST_FORWARD_ATTEMPTS) { + val currentDeadline = SystemClock.elapsedRealtime() + COOLDOWN_SECONDS * 1000 + preferences[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] = currentDeadline + } + } + } + + override suspend fun resetAttempts(hotWalletId: HotWalletId) { + val authAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = true, + ) + val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = false, + ) + + appPreferencesStore.editData { + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Flow { + val flow = appPreferencesStore.data.map { + AttemptsPersistentData( + attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0, + bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0, + deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L, + ) + }.distinctUntilChanged() + + return flow.transformLatest { + while (true) { + emit(toState(id, it.attempts, it.deadline, it.bootCount)) + val remaining = remainingSeconds(it.deadline, it.bootCount) + if (remaining <= 0) break + delay(timeMillis = 1000) + } + }.distinctUntilChanged() + } + + override suspend fun getAttemptsSync(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Attempts { + val prefs = appPreferencesStore.data.first() + val count = prefs[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0 + val boot = prefs[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0 + val deadline = prefs[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L + return toState(id, count, deadline, boot) + } + + private fun remainingSeconds(deadline: Long, bootStored: Int): Int { + val now = SystemClock.elapsedRealtime() + val bootNow = currentBootCount() + if (bootNow != bootStored) { + // If the boot happened after the last attempt, we consider timer to start from the beginning + return maxOf(0, COOLDOWN_SECONDS - (now / 1000).toInt()) + } + return maxOf(0, ((deadline - now) / 1000).toInt()) + } + + private fun toState( + id: HotWalletAccessCodeAttemptsRepository.AttemptId, + count: Int, + deadlineElapsed: Long, + bootStored: Int, + ): Attempts { + val fast = MAX_FAST_FORWARD_ATTEMPTS + val attention = ATTEMPTS_BEFORE_DELETION + val deletion = MAX_ATTEMPTS_BEFORE_DELETION + + return when { + count < fast -> Attempts.FastForward(count) + id.auth && count >= deletion -> Attempts.Deletion + id.auth && count >= attention -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.BeforeDeletion(count, remaining, deletion - count) + } + else -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.WithDelay(count, remaining) + } + } + } + + private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { + return "${hotWalletId.value}_$auth" + } + + private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) + + private data class AttemptsPersistentData( + val attempts: Int, + val bootCount: Int, + val deadline: Long, + ) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt new file mode 100644 index 0000000000..b5ca40b2de --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -0,0 +1,175 @@ +package com.tangem.data.wallets.hot + +import com.tangem.common.core.TangemSdkError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.copy +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.exception.WrongPasswordException +import com.tangem.hot.sdk.model.* +import javax.inject.Inject + +class HotWalletAccessor @Inject constructor( + private val tangemHotSdk: TangemHotSdk, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletPasswordRequester: HotWalletPasswordRequester, + private val walletsRepository: WalletsRepository, +) { + + suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = + hotSdkRequest(hotWalletId) { unlock -> + tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign) + } + + suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse = + hotSdkRequest(hotWalletId) { unlock -> + tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request) + } + + private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + + val auth = when (hotWalletId.authType) { + HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth + HotWalletId.AuthType.Password -> requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) + HotWalletId.AuthType.Biometry -> { + if (isAccessCodeRequired) { + requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) + } else { + HotAuth.Biometry + } + } + } + + return runCatchingSdkErrors(hotWalletId, auth) { + block(UnlockHotWallet(hotWalletId, it)).also { + hotWalletPasswordRequester.dismiss() + } + } + } + + private suspend fun runCatchingSdkErrors( + hotWalletId: HotWalletId, + auth: HotAuth, + block: suspend (auth: HotAuth) -> T, + ): T { + return runCatchingWrongPassInternal( + hotWalletId = hotWalletId, + originalAuth = auth, + auth = auth, + block = { blockAuth -> + block(blockAuth).also { + // Update biometry auth if the original auth was password + updateBiometryAuthIfNeeded( + hotWalletId = hotWalletId, + originalAuth = blockAuth, + ) + } + }, + ) + } + + private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + + if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) { + val userWallet = userWalletsListRepository.userWalletsSync() + .find { it is UserWallet.Hot && it.hotWalletId == hotWalletId } + as? UserWallet.Hot + ?: return + + val newHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = originalAuth, + ), + auth = HotAuth.Biometry, + ) + + userWalletsListRepository.saveWithoutLock( + userWallet = userWallet.copy( + hotWalletId = newHotWalletId, + ), + canOverride = true, + ) + } + } + + private suspend fun runCatchingWrongPassInternal( + hotWalletId: HotWalletId, + originalAuth: HotAuth, + auth: HotAuth, + block: suspend (auth: HotAuth) -> T, + ): T = runCatching { + block(auth) + }.getOrElse { exception -> + if (auth is HotAuth.Biometry && exception.isBiometryError()) { + // fallback to password if biometry fails + val passAuth = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = true, + ) + + return@getOrElse runCatchingWrongPassInternal( + hotWalletId = hotWalletId, + originalAuth = originalAuth, + auth = passAuth, + block = block, + ) + } + + if (exception !is WrongPasswordException) { + throw exception + } + + // If the exception is a wrong password, we need to request the password again + + hotWalletPasswordRequester.wrongPassword() + val passResult = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = originalAuth is HotAuth.Biometry, + ) + + runCatchingWrongPassInternal( + hotWalletId = hotWalletId, + originalAuth = originalAuth, + auth = passResult, + block = block, + ) + } + + private suspend fun requestPassword(hotWalletId: HotWalletId, hasBiometry: Boolean): HotAuth { + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = false, + hasBiometry = hasBiometry, + ) + + return hotWalletPasswordRequester.requestPassword(attemptRequest).toAuth() + ?: throw TangemSdkError.UserCancelled() + } + + private fun Throwable.isBiometryError(): Boolean { + return this is TangemSdkError.AuthenticationFailed || + this is TangemSdkError.AuthenticationCanceled || + this is TangemSdkError.AuthenticationLockout || + this is TangemSdkError.AuthenticationUnavailable || + this is TangemSdkError.AuthenticationAlreadyInProgress || + this is TangemSdkError.AuthenticationNotInitialized || + this is TangemSdkError.AuthenticationPermanentLockout + } + + private fun HotWalletPasswordRequester.Result.toAuth() = when (this) { + HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry + HotWalletPasswordRequester.Result.Dismiss -> null + is HotWalletPasswordRequester.Result.EnteredPassword -> this.password + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt similarity index 99% rename from app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt index ba64c2fda2..b5bfc9da12 100644 --- a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.hot +package com.tangem.data.wallets.hot import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.Wallet diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 2911dd5968..447c2effdf 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -195,7 +195,7 @@ class DefaultWalletsRepositoryTest { ) val authProvider = mockk { - every { getCardsPublicKeys() } returns publicKeys + coEvery { getCardsPublicKeys() } returns publicKeys } repository = DefaultWalletsRepository( diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt similarity index 80% rename from app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt rename to data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt index f5484b4b50..eb812166f7 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import android.annotation.SuppressLint import com.google.common.truth.Truth @@ -7,6 +7,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.configs.GenericCardConfig @@ -14,7 +15,7 @@ import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse -import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager +import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -27,13 +28,17 @@ import org.junit.Test */ internal class DefaultDerivationsRepositoryTest { - private val tangemSdkManager = mockk() + private val tangemSdkManager = mockk() private val userWalletsStore = mockk() private val repository = DefaultDerivationsRepository( - tangemSdkManager = tangemSdkManager, userWalletsStore = userWalletsStore, dispatchers = TestingCoroutineDispatcherProvider(), - networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()), + hotDerivationsRepository = mockk(), + coldDerivationsRepository = DefaultColdMapDerivationsRepository( + tangemSdkManager = tangemSdkManager, + networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()), + dispatchers = TestingCoroutineDispatcherProvider(), + ), ) private val defaultUserWalletId = UserWalletId("011") @@ -48,7 +53,7 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `error if userWalletId not found`() = runTest { - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns null + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } throws IllegalStateException() runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) @@ -56,7 +61,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { error("Should throws exception") } .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -64,13 +69,17 @@ internal class DefaultDerivationsRepositoryTest { @SuppressLint("CheckResult") @Test fun `success if card is not supported derivations`() = runTest { - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns defaultUserWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet - runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) } + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) + + runCatching { } .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success") } + .onFailure { + error("Should returns success") + } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -81,13 +90,13 @@ internal class DefaultDerivationsRepositoryTest { val userWallet = defaultUserWallet.copy( scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) } .onSuccess { Truth.assertThat(it) } .onFailure { error("Should returns success") } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -102,7 +111,7 @@ internal class DefaultDerivationsRepositoryTest { ), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet runCatching { repository.derivePublicKeys( @@ -113,7 +122,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { Truth.assertThat(it) } .onFailure { error("Should returns success") } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -123,7 +132,7 @@ internal class DefaultDerivationsRepositoryTest { val userWallet = defaultUserWallet.copy( scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled runCatching { @@ -135,7 +144,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { error("Should throws exception") } .onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -146,7 +155,7 @@ internal class DefaultDerivationsRepositoryTest { val userWallet = defaultUserWallet.copy( scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success( DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys), ) @@ -161,7 +170,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { Truth.assertThat(it) } .onFailure { error("Should returns success but $it") } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) } } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DerivedKeysMocks.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DerivedKeysMocks.kt similarity index 95% rename from app/src/test/kotlin/com/tangem/tap/domain/card/DerivedKeysMocks.kt rename to data/wallets/src/test/java/com/tangem/data/wallets/derivations/DerivedKeysMocks.kt index a8398c930c..ae0e28b70b 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DerivedKeysMocks.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DerivedKeysMocks.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationConfigV2 diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt similarity index 90% rename from app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt rename to data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt index f623b13c56..426a1bc144 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import com.google.common.truth.Truth import com.tangem.blockchain.blockchains.cardano.CardanoUtils @@ -13,7 +13,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.Wallet2CardConfig -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import org.junit.Test /** @@ -24,7 +24,8 @@ internal class MissedDerivationsFinderTest { @Test fun `empty derivations for empty currencies`() { val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) - val finder = MissedDerivationsFinder(scanResponse) + val userWallet = MockUserWalletFactory.create(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val actual = finder.find(emptyList()) @@ -36,7 +37,7 @@ internal class MissedDerivationsFinderTest { // Bls is not supported val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf) val actual = finder.find(currencies) @@ -58,7 +59,7 @@ internal class MissedDerivationsFinderTest { ) } val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum val actual = finder.find(currencies) @@ -73,7 +74,7 @@ internal class MissedDerivationsFinderTest { fun `derivations for custom token`() { val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation val actual = finder.find(currencies) @@ -91,7 +92,7 @@ internal class MissedDerivationsFinderTest { fun `derivations for cardano`() { val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) @@ -117,7 +118,7 @@ internal class MissedDerivationsFinderTest { derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf) val actual = finder.find(currencies) @@ -132,7 +133,7 @@ internal class MissedDerivationsFinderTest { derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar val actual = finder.find(currencies) diff --git a/domain/account/.gitignore b/domain/account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts new file mode 100644 index 0000000000..75db105c86 --- /dev/null +++ b/domain/account/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + + api(projects.domain.core) + api(projects.domain.models) + api(projects.domain.wallets.models) + + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.kotlin.serialization) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt new file mode 100644 index 0000000000..319babf91b --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -0,0 +1,177 @@ +package com.tangem.domain.account.models + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.extensions.addOrReplace +import kotlinx.serialization.Serializable + +/** + * Represents a list of accounts associated with a user wallet + * + * @property userWallet the user wallet associated with the account list + * @property accounts a set of accounts belonging to the user wallet + * @property totalAccounts the total number of accounts + * +[REDACTED_AUTHOR] + */ +@Serializable +data class AccountList private constructor( + val userWallet: UserWallet, + val accounts: Set, + val totalAccounts: Int, +) { + + /** Retrieves the main crypto portfolio account from the list of accounts */ + val mainAccount: Account.CryptoPortfolio + get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio + + /** Returns true if more accounts can be added (the maximum number of accounts has not been reached) */ + val canAddMoreAccounts: Boolean + get() = accounts.size < MAX_ACCOUNTS_COUNT + + /** + * Adds an account to the account list. + * If an account with the same identifier already exists, it will be replaced. + * Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are + * violated (e.g., maximum number of accounts exceeded). + * + * @param other the account to add or replace + */ + operator fun plus(other: Account): Either { + val isNewAccount = this.accounts.none { it.accountId == other.accountId } + val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId } + + return invoke( + userWallet = this.userWallet, + accounts = accounts, + totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + ) + } + + /** + * Removes the specified account from the account list. + * Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are + * violated (e.g., the list becomes empty). + * + * @param other the account to remove + */ + operator fun minus(other: Account): Either { + val isExistingAccount = this.accounts.any { it.accountId == other.accountId } + val accounts = this.accounts.toMutableSet().apply { + removeIf { it.accountId == other.accountId } + } + + return invoke( + userWallet = this.userWallet, + accounts = accounts, + totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, + ) + } + + /** + * Represents possible errors that can occur when creating an `AccountList` + */ + @Serializable + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "AccountListError" + + @Serializable + data object EmptyAccountsList : Error { + override fun toString(): String = "$tag: The accounts list cannot be empty" + } + + @Serializable + data object MainAccountNotFound : Error { + override fun toString(): String { + return "$tag: Account list does not contain a main crypto portfolio account" + } + } + + @Serializable + data object ExceedsMaxMainAccountsCount : Error { + override fun toString(): String { + return "$tag: There should be at most one main crypto portfolio in the account list" + } + } + + @Serializable + data object ExceedsMaxAccountsCount : Error { + override fun toString(): String = "$tag: The number of accounts must not exceed 20" + } + + @Serializable + data object DuplicateAccountIds : Error { + override fun toString(): String = "$tag: Account list contains duplicate account IDs" + } + + @Serializable + data object DuplicateAccountNames : Error { + override fun toString(): String = "$tag: Account list contains duplicate account names" + } + } + + companion object { + + private const val MAX_ACCOUNTS_COUNT = 20 + private const val MAX_MAIN_ACCOUNTS_COUNT = 1 + + /** + * Factory method to create an `AccountList` instance. + * Validates the input to ensure the accounts list is not empty and contains exactly one main account. + * + * @param userWallet the user wallet associated with the account list + * @param accounts a set of accounts belonging to the user wallet + * @param totalAccounts the total number of accounts + */ + operator fun invoke( + userWallet: UserWallet, + accounts: Set, + totalAccounts: Int, + ): Either = either { + ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } + + ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount } + + val mainAccountsCount = accounts.mainAccountsCount() + ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) { + if (mainAccountsCount == 0) { + Error.MainAccountNotFound + } else { + Error.ExceedsMaxMainAccountsCount + } + } + + val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size + ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } + + val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size + ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames } + + AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) + } + + /** + * Factory method to create an empty [AccountList] with a main crypto portfolio account + * + * @param userWallet the user wallet associated with the account list + */ + fun empty(userWallet: UserWallet): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf( + Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId), + ), + totalAccounts = 1, + ) + } + + private fun Set.mainAccountsCount(): Int { + return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true } + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt new file mode 100644 index 0000000000..acd8f76d35 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.account.models + +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.wallet.UserWallet +import kotlinx.serialization.Serializable + +/** + * Represents a list of account statuses associated with a user wallet + * + * @property userWallet the user wallet to which the account statuses belong + * @property accountStatuses a set of account statuses associated with the user wallet + * @property totalAccounts the total number of accounts + * +[REDACTED_AUTHOR] + */ +@Serializable +data class AccountStatusList( + val userWallet: UserWallet, + val accountStatuses: Set, + val totalAccounts: Int, +) \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt b/domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt new file mode 100644 index 0000000000..1c017a4f88 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.account.models + +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import kotlinx.serialization.Serializable + +/** + * Represents an archived crypto portfolio account + * + * @property accountId the unique identifier of the archived account + * @property name the name of the archived account + * @property icon the icon representing the archived account + * @property derivationIndex the derivation index for the archived account + * @property tokensCount the number of tokens in the archived account + * @property networksCount the number of networks associated with the archived account + * +[REDACTED_AUTHOR] + */ +@Serializable +data class ArchivedAccount( + val accountId: AccountId, + val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: DerivationIndex, + val tokensCount: Int, + val networksCount: Int, +) \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt new file mode 100644 index 0000000000..ac6921e167 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -0,0 +1,85 @@ +package com.tangem.domain.account.repository + +import arrow.core.Option +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository interface for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +interface AccountsCRUDRepository { + + /** + * Retrieves a list of accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing the [AccountList] if found, or `Option.None` if not + */ + suspend fun getAccounts(userWalletId: UserWalletId): Option + + /** + * Retrieves a specific account by its unique identifier + * + * @param accountId the unique identifier of the account + * @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not + */ + suspend fun getAccount(accountId: AccountId): Option + + /** + * Retrieves a archived account by its unique identifier + * + * @param accountId the unique identifier of the account + */ + suspend fun getArchivedAccount(accountId: AccountId): Option + + /** + * Retrieves a list of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not + */ + suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> + + /** + * Provides a flow of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + fun getArchivedAccounts(userWalletId: UserWalletId): Flow> + + /** + * Fetches archived accounts for a specific user wallet and updates the repository + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) + + /** + * Saves a list of accounts to the repository + * + * @param accountList the list of accounts to be saved. + */ + suspend fun saveAccounts(accountList: AccountList) + + /** + * Retrieves the total count of accounts associated with a specific user wallet including archived accounts + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int + + /** + * Retrieves a user wallet by its unique identifier + * + * @param userWalletId the unique identifier of the user wallet + * @return the [UserWallet] associated with the given identifier + */ + fun getUserWallet(userWalletId: UserWalletId): UserWallet +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..75722f9824 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -0,0 +1,120 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.Option +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.* +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for adding a new crypto portfolio account + * + * @property crudRepository the repository used for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class AddCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Adds a new crypto portfolio account to the repository + * + * @param userWalletId the unique identifier of the user wallet + * @param accountName the name of the new account + * @param icon the icon representing the new account + * @param derivationIndex the derivation index for the account + * + + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + accountName: AccountName, + icon: CryptoPortfolioIcon, + derivationIndex: DerivationIndex, + ): Either = either { + val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) + + val accountList = getAccountList(userWalletId = userWalletId).getOrElse { + createNewAccountList(userWalletId = userWalletId) + } + + val updatedAccounts = (accountList + newAccount).getOrElse { + raise(Error.AccountListRequirementsNotMet(it)) + } + + saveAccounts(updatedAccounts) + + newAccount + } + + private fun Raise.createAccount( + userWalletId: UserWalletId, + accountName: AccountName, + icon: CryptoPortfolioIcon, + derivationIndex: DerivationIndex, + ): Account.CryptoPortfolio { + return Account.CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), + accountName = accountName, + accountIcon = icon, + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + } + + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): Option { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + private fun Raise.createNewAccountList(userWalletId: UserWalletId): AccountList { + val userWallet = catch( + block = { crudRepository.getUserWallet(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + + return AccountList.empty(userWallet = userWallet) + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the add operation + */ + sealed interface Error { + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "Account list requirements not met: $cause" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..0611b106fb --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -0,0 +1,102 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for archiving a crypto portfolio. + * This class provides functionality to archive a specific account within a user's crypto portfolio. + * It ensures that the account exists and meets the necessary requirements before performing the operation. + * + * @property crudRepository repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class ArchiveCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** Archives the specified account by its [accountId] */ + suspend operator fun invoke(accountId: AccountId): Either = either { + val accountList = getAccountList(userWalletId = accountId.userWalletId) + + val archivingAccount = accountList.accounts + .firstOrNull { it.accountId == accountId } + ?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + + val updatedAccounts = (accountList - archivingAccount).getOrElse { + raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) + } + + saveAccounts(updatedAccounts) + } + + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the archiving process + */ + sealed interface Error { + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$this: Data operation failed: ${cause.message ?: "Unknown error"}" + } + + /** + * Represents critical technical errors that can occur during the update operation. + * These errors are a consequence of an inconsistent state. + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + + override fun toString(): String { + return "${this.javaClass.simpleName}: Accounts for $userWalletId are not created" + } + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "${this.javaClass.simpleName}: Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError { + + override fun toString(): String { + return "${this.javaClass.simpleName}: Account list requirements not met: $cause" + } + } + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt new file mode 100644 index 0000000000..cbcfb13168 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.launch + +typealias ArchivedAccountList = List + +/** + * Use case for retrieving archived accounts for a specific user wallet + * + * @property crudRepository the repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetArchivedAccountsUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Executes the use case to retrieve archived accounts for the given user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + operator fun invoke(userWalletId: UserWalletId): LceFlow = channelFlow { + val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId) + + archivedAccounts + .onRight { send(it.lceContent()) } + .onLeft { + send(lceLoading()) + + launch { + fetchArchivedAccounts(userWalletId).getOrElse { + send(it.lceError()) + } + } + } + + subscribeOnArchivedAccounts(userWalletId) + } + .distinctUntilChanged() + + private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { + crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse { + error("Archived accounts not found for user wallet: $userWalletId") + } + } + } + + private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) } + } + + private suspend fun ProducerScope>.subscribeOnArchivedAccounts( + userWalletId: UserWalletId, + ) { + crudRepository.getArchivedAccounts(userWalletId) + .distinctUntilChanged() + .retryWhen { cause, _ -> + send(cause.lceError()) + + delay(timeMillis = 2000) + + true + } + .collectLatest { archivedAccounts -> + send(archivedAccounts.lceContent()) + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt new file mode 100644 index 0000000000..c34240e22b --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for retrieving the next unoccupied account index + * + * @property crudRepository repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetUnoccupiedAccountIndexUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Invokes the use case to calculate the next unoccupied account index + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val totalAccountsCount = getTotalAccountsCount(userWalletId = userWalletId) + + DerivationIndex(totalAccountsCount + 1).getOrElse { + raise(Error.InvalidDerivationIndex(it)) + } + } + + private suspend fun Raise.getTotalAccountsCount(userWalletId: UserWalletId): Int { + return catch( + block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur in the use case + */ + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error" + + /** Error indicating that the derivation index is invalid */ + data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error { + override fun toString(): String = "$tag: Invalid derivation index: $cause" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..9679a036b6 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -0,0 +1,129 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for recovering a crypto portfolio account from archived accounts + * + * @property crudRepository repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class RecoverCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Recovers a crypto portfolio account by moving it from archived accounts to active accounts + * + * @param accountId the unique identifier of the account to recover + */ + suspend operator fun invoke(accountId: AccountId): Either = either { + val accountList = getAccountList(userWalletId = accountId.userWalletId) + val archivedAccount = getArchivedAccount(accountId = accountId) + + val recoveredAccount = archivedAccount.recover() + + val updatedAccountList = (accountList + recoveredAccount) + .getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) } + + saveAccounts(updatedAccountList) + + recoveredAccount + } + + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.getArchivedAccount(accountId: AccountId): ArchivedAccount { + return catch( + block = { crudRepository.getArchivedAccount(accountId = accountId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { + raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + } + } + + private fun ArchivedAccount.recover(): Account.CryptoPortfolio { + return Account.CryptoPortfolio( + accountId = this.accountId, + accountName = this.name, + accountIcon = this.icon, + derivationIndex = this.derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the add operation + */ + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error" + + /** + * Critical technical errors that can occur during the recovery operation + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + override fun toString(): String = "$tag: Accounts for $userWalletId are not created" + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "$tag: Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "$tag: Account list requirements not met: $cause" + } + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..8c2208e552 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -0,0 +1,130 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for updating a crypto portfolio account. + * + * @property crudRepository the repository used for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class UpdateCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Updates a crypto portfolio account with the provided name and/or icon + * + * @param accountId the unique identifier of the account to update + * @param accountName the new name for the account (optional) + * @param icon the new icon for the account (optional) + * @return an [Either] containing the updated [Account.CryptoPortfolio] on success, or an [Error] on failure + */ + suspend operator fun invoke( + accountId: AccountId, + accountName: AccountName? = null, + icon: CryptoPortfolioIcon? = null, + ): Either = either { + ensure(accountName != null || icon != null) { Error.NothingToUpdate } + + val accountList = getAccountList(userWalletId = accountId.userWalletId) + + val account = accountList.accounts + .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio + ?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + + val updatedAccount = account + .setName(name = accountName) + .setIcon(icon = icon) + + val updatedAccounts = (accountList + updatedAccount).getOrElse { + raise(Error.CriticalTechError.AccountListRequirementsNotMet(it)) + } + + saveAccounts(updatedAccounts) + + updatedAccount + } + + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + private fun Account.CryptoPortfolio.setName(name: AccountName?): Account.CryptoPortfolio { + return if (name != null) this.copy(accountName = name) else this + } + + private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio { + return if (icon != null) this.copy(accountIcon = icon) else this + } + + /** + * Represents possible errors that can occur during the update operation + */ + sealed interface Error { + + /** Error indicating that there is nothing to update */ + data object NothingToUpdate : Error { + override fun toString(): String = "Nothing to update: both account name and icon are null" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + } + + /** + * Represents critical technical errors that can occur during the update operation. + * These errors are a consequence of an inconsistent state. + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + override fun toString(): String = "Accounts for $userWalletId are not created" + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError { + override fun toString(): String = "Account list requirements not met: $cause" + } + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt new file mode 100644 index 0000000000..d5323c0a85 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -0,0 +1,346 @@ +package com.tangem.domain.account.models + +import arrow.core.Either +import arrow.core.left +import com.google.common.truth.Truth +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.account.utils.createAccounts +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountListTest { + + @Test + fun mainAccount() { + // Arrange + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(mainAccount), + totalAccounts = 1, + ) + .getOrNull()!! + + // Act + val actual = accountList.mainAccount + + // Assert + val expected = mainAccount + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun canAddMoreAccounts() { + // Arrange + val accountList = AccountList( + userWallet = mockk(), + accounts = createAccounts(userWalletId = userWalletId, count = 2), + totalAccounts = 2, + ).getOrNull()!! + + val fullAccountList = AccountList( + userWallet = mockk(), + accounts = createAccounts(userWalletId = userWalletId, count = 20), + totalAccounts = 20, + ).getOrNull()!! + + // Act & Assert + Truth.assertThat(accountList.canAddMoreAccounts).isTrue() + Truth.assertThat(fullAccountList.canAddMoreAccounts).isFalse() + } + + @Test + fun empty() { + // Arrange + val userWallet = mockk(relaxed = true) + + // Act + val actual = AccountList.empty(userWallet) + + // Assert + val expected = AccountList( + userWallet = userWallet, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + totalAccounts = 1, + ).getOrNull()!! + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Create { + + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(userWallet) + } + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: CreateTestModel) { + // Act + val actual = AccountList( + userWallet = userWallet, + accounts = model.accounts, + totalAccounts = model.accounts.size, + ) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + CreateTestModel( + accounts = emptySet(), + expected = AccountList.Error.EmptyAccountsList.left(), + ), + CreateTestModel( + accounts = setOf( + createAccount(userWalletId = userWalletId, derivationIndex = 1), + ), + expected = AccountList.Error.MainAccountNotFound.left(), + ), + CreateTestModel( + accounts = setOf( + Account.CryptoPortfolio.createMainAccount(userWalletId), + Account.CryptoPortfolio.createMainAccount(userWalletId).copy( + accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + ), + ), + expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), + ), + createAccounts(userWalletId = userWalletId, count = 1).let { + CreateTestModel( + accounts = it, + expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1), + ) + }, + createAccounts(userWalletId = userWalletId, count = 20).let { + CreateTestModel( + accounts = it, + expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20), + ) + }, + CreateTestModel( + accounts = createAccounts(userWalletId = userWalletId, count = 21), + expected = AccountList.Error.ExceedsMaxAccountsCount.left(), + ), + CreateTestModel( + accounts = setOf( + createAccount(userWalletId = userWalletId, derivationIndex = 0), + createAccount(userWalletId = userWalletId, derivationIndex = 1), + createAccount(userWalletId = userWalletId, derivationIndex = 1), + ), + expected = AccountList.Error.DuplicateAccountIds.left(), + ), + CreateTestModel( + accounts = setOf( + createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 0), + createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 1), + ), + expected = AccountList.Error.DuplicateAccountNames.left(), + ), + ) + } + + data class CreateTestModel( + val accounts: Set, + val expected: Either, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Plus { + + private val userWallet = mockk() + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: PlusTestModel) { + // Act + val actual = model.initial.plus(other = model.toAdd) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + // region Add new account + run { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val newAccount = createAccount(userWalletId = userWalletId, derivationIndex = 1) + + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = newAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, newAccount), + totalAccounts = 2, + ), + ) + }, + // endregion + // region Replace existing account + run { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val newAccount = mainAccount.copy(accountName = AccountName("New Name").getOrNull()!!) + + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = newAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(newAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = createAccounts(userWalletId = userWalletId, count = 20), + totalAccounts = 20, + ).getOrNull()!!, + toAdd = createAccount(userWalletId = userWalletId, derivationIndex = 21), + expected = AccountList.Error.ExceedsMaxAccountsCount.left(), + ), + ) + } + + data class PlusTestModel( + val initial: AccountList, + val toAdd: Account, + val expected: Either, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Minus { + + private val userWallet = mockk() + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: MinusTestModel) { + // Act + val actual = model.initial.minus(model.toRemove) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + // region Remove existing account + run { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, secondaryAccount), + totalAccounts = 2, + ).getOrNull()!!, + toRemove = secondaryAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + // region Remove unexisting account + run { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val notInList = createAccount(userWalletId = userWalletId, derivationIndex = 3) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toRemove = notInList, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + // region EmptyAccountsList + run { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toRemove = mainAccount, + expected = AccountList.Error.EmptyAccountsList.left(), + ) + }, + // endregion + // region MainAccountNotFound + run { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, secondaryAccount), + totalAccounts = 2, + ).getOrNull()!!, + toRemove = mainAccount, + expected = AccountList.Error.MainAccountNotFound.left(), + ) + }, + // endregion + ) + } + + data class MinusTestModel( + val initial: AccountList, + val toRemove: Account, + val expected: Either, + ) + + private companion object { + + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..cf47f9b807 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,203 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.account.utils.createAccounts +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AddCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = AddCryptoPortfolioUseCase(crudRepository) + + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should add new crypto portfolio account to existing list`() = runTest { + // Arrange + val newAccount = createNewAccount() + val accountList = AccountList.empty(userWallet) + val updatedAccountList = (accountList + newAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + + coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } + } + + @Test + fun `invoke should create new account list if none exists`() = runTest { + // Arrange + val newAccount = createNewAccount() + val newAccountList = (AccountList.empty(userWallet) + newAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns None + coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getUserWallet(userWalletId) + crudRepository.saveAccounts(newAccountList) + } + } + + @Test + fun `invoke should return error if account list requirements not met`() = runTest { + // Arrange + val accountList = AccountList( + userWallet = userWallet, + accounts = createAccounts(userWalletId = userWalletId, count = 20), + totalAccounts = 20, + ).getOrNull()!! + + val newAccount = createNewAccount(derivationIndex = 21) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet( + cause = AccountList.Error.ExceedsMaxAccountsCount, + ).left() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + + coVerify(inverse = true) { + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val newAccount = createNewAccount() + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + + coVerify(inverse = true) { + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + // Arrange + val newAccount = createNewAccount() + val accountList = AccountList.empty(userWallet) + val updatedAccountList = (accountList + newAccount).getOrNull()!! + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + + coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } + } + + private companion object { + + val userWalletId = UserWalletId("011") + + fun createNewAccount(derivationIndex: Int = 1): Account.CryptoPortfolio { + return createAccount( + userWalletId = userWalletId, + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + ) + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..aaea0a5379 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,157 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.Error +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchiveCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository) + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should archive existing crypto portfolio account`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountId = account.accountId + + val archivedAccount = account.copy(isArchived = true) + val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke should return error if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns None + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if account not found`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet) + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex(1).getOrNull()!!, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountId = account.accountId + + val archivedAccount = account.copy(isArchived = true) + val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt new file mode 100644 index 0000000000..eb0019f93c --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt @@ -0,0 +1,158 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetArchivedAccountsUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetArchivedAccountsUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should emit archived accounts when repository returns data`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption() + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf(archivedAccounts.lceContent()) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + + coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) } + } + + @Test + fun `invoke should emit loading and fetch when accounts not found`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Fetch error") + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow() + coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + exception.lceError(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun TestScope.getEmittedValues(flow: Flow): List { + val values = mutableListOf() + + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(values) + } + + return values + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt new file mode 100644 index 0000000000..ec8af7f2b7 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.account.usecase + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetUnoccupiedAccountIndexUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetUnoccupiedAccountIndexUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should return next unoccupied index when repository returns count`() = runTest { + // Arrange + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3 + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = 4.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } + + @Test + fun `invoke should return error if repository throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } throws exception + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..316e789754 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,208 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RecoverCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = RecoverCryptoPortfolioUseCase(crudRepository) + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should recover archived crypto portfolio account`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + val archivedAccount = ArchivedAccount( + accountId = account.accountId, + name = account.name, + icon = account.icon, + derivationIndex = account.derivationIndex, + tokensCount = 1, + networksCount = 1, + ) + + val recoveredAccount = account.copy(isArchived = false) + val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = recoveredAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke should return error if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns None + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { + crudRepository.getArchivedAccount(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { + crudRepository.getArchivedAccount(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getArchivedAccount throws exception`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } throws exception + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if getArchivedAccount returns null`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } returns None + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + val archivedAccount = ArchivedAccount( + accountId = account.accountId, + name = account.name, + icon = account.icon, + derivationIndex = account.derivationIndex, + tokensCount = 1, + networksCount = 1, + ) + + val recoveredAccount = account.copy(isArchived = false) + val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..d6638b5c05 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,247 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase.Error +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class UpdateCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository) + + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should update crypto portfolio account with new name`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId + + val newAccountName = AccountName("New name").getOrNull()!! + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke should update crypto portfolio account with new icon`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId + + val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.CaribbeanBlue, + ) + val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, icon = newAccountIcon) + + // Assert + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke should update crypto portfolio account with new name and icon`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId + + val newAccountName = AccountName("New name").getOrNull()!! + val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.CaribbeanBlue, + ) + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon) + + // Assert + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke if name and icon are null`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId) + + // Assert + val expected = Error.NothingToUpdate.left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(inverse = true) { + crudRepository.getAccounts(userWalletId = any()) + crudRepository.saveAccounts(accountList = any()) + } + } + + @Test + fun `invoke if getAccounts throws exception`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId + + val newAccountName = AccountName("New name").getOrNull()!! + + val exception = IllegalStateException("Test exception") + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + val accountList = None + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if getAccounts does not contain accountId`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex(1).getOrNull()!!, + ) + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if saveAccounts throws exception`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId + + val newAccountName = AccountName("New name").getOrNull()!! + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(accountList = updatedAccountList) } throws exception + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + private companion object { + + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt new file mode 100644 index 0000000000..597d6aa059 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -0,0 +1,45 @@ +package com.tangem.domain.account.utils + +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.* +import com.tangem.domain.models.wallet.UserWalletId +import kotlin.random.Random + +fun createAccounts(userWalletId: UserWalletId, count: Int): Set { + return buildSet { + add(Account.CryptoPortfolio.createMainAccount(userWalletId)) + + repeat(count - 1) { + val account = createAccount( + userWalletId = userWalletId, + name = "Test Account ${it + 1}", + derivationIndex = it + 1, + ) + + add(account) + } + } +} + +fun createAccount( + userWalletId: UserWalletId, + name: String = "Test Account", + icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex: Int = Random.nextInt(1, 21), +): Account.CryptoPortfolio { + val derivationIndex = DerivationIndex(derivationIndex).getOrNull()!! + + return Account.CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), + accountName = AccountName(name).getOrNull()!!, + accountIcon = icon, + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt index f98a8ed3a7..e8badaf526 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Blockchain.Companion.fromId import com.tangem.blockchain.common.Token import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion @@ -76,7 +77,7 @@ internal class TangemCardTypesResolver( } else { return Blockchain.Unknown } - Blockchain.Companion.fromBlockchainName(blockchainName) + Blockchain.fromBlockchainName(blockchainName) } } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt new file mode 100644 index 0000000000..40ce107753 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.card.analytics + +internal sealed class AnalyticsParam { + + sealed class CurrencyType(val value: String) { + class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency) + class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt new file mode 100644 index 0000000000..0cb4a3685c --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.card.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class IntroductionProcess( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Introduction Process", event, params) { + + object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") + object ButtonTokensList : IntroductionProcess("Button - Tokens List") + object ButtonBuyCards : IntroductionProcess("Button - Buy Cards") + object ButtonScanCard : IntroductionProcess("Button - Scan Card") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt index c5c7e0ea3c..2398da3650 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt @@ -1,18 +1,14 @@ -package com.tangem.tap.common.analytics.converters +package com.tangem.domain.card.analytics import com.tangem.blockchain.common.Blockchain +import com.tangem.core.analytics.models.AnalyticsParam.WalletType import com.tangem.domain.card.CardTypesResolver -import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam -/** -[REDACTED_AUTHOR] - */ -class ParamCardCurrencyConverter : Converter { +class ParamCardCurrencyConverter : Converter { - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency + override fun convert(value: CardTypesResolver): WalletType? { + if (value.isMultiwalletAllowed()) return WalletType.MultiCurrency val type = when { value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) @@ -22,6 +18,6 @@ class ParamCardCurrencyConverter : Converter null } ?: return null - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) + return WalletType.SingleCurrency(type.value) } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt new file mode 100644 index 0000000000..62c02a56ab --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.card.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class Shop( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Shop", event, params) { + + object ScreenOpened : Shop("Shop Screen Opened") +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt index 32bc4a3ecc..a189ade273 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt @@ -100,6 +100,20 @@ fun UserWallet.canHandleToken(blockchain: Blockchain, excludedBlockchains: Exclu } } +fun UserWallet.canHandleBlockchain(blockchain: Blockchain, excludedBlockchains: ExcludedBlockchains): Boolean { + return when (this) { + is UserWallet.Cold -> { + scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + excludedBlockchains = excludedBlockchains, + cardTypesResolver = scanResponse.cardTypesResolver, + ) + } + is UserWallet.Hot -> blockchain.isTestnet().not() && + blockchain !in excludedBlockchains + } +} + /** * The same as [CardDTO.supportedTokens] but with supportedTokens input, if previously calculated */ diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt index 6fb8c34eb4..0a6e22e8e5 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt @@ -6,10 +6,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.CardTypesResolver -import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.card.TangemCardTypesResolver -import com.tangem.domain.card.TangemDerivationStyleProvider -import com.tangem.domain.card.TangemHotDerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.configs.CardConfig @@ -25,18 +22,6 @@ val ScanResponse.cardTypesResolver: CardTypesResolver walletData = walletData, ) -val UserWallet.derivationStyleProvider: DerivationStyleProvider - get() = when (this) { - is UserWallet.Cold -> this.scanResponse.derivationStyleProvider - is UserWallet.Hot -> TangemHotDerivationStyleProvider() - } - -val ScanResponse.derivationStyleProvider: DerivationStyleProvider - get() = card.derivationStyleProvider - -val CardDTO.derivationStyleProvider: DerivationStyleProvider - get() = TangemDerivationStyleProvider(this) - val UserWallet.Cold.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt index 346a09dc81..58c029cccb 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt @@ -207,6 +207,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.ZkLinkNovaTestnet -> EllipticCurve.Secp256k1 Blockchain.Pepecoin -> EllipticCurve.Secp256k1 Blockchain.PepecoinTestnet -> EllipticCurve.Secp256k1 + Blockchain.Hyperliquid -> EllipticCurve.Secp256k1 + Blockchain.HyperliquidTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt index 152e79a017..927e7f0aa1 100644 --- a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt +++ b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt @@ -164,6 +164,8 @@ class Wallet2CardConfigTest { Blockchain.ZkLinkNovaTestnet to EllipticCurve.Secp256k1, Blockchain.Pepecoin to EllipticCurve.Secp256k1, Blockchain.PepecoinTestnet to EllipticCurve.Secp256k1, + Blockchain.Hyperliquid to EllipticCurve.Secp256k1, + Blockchain.HyperliquidTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 9910d828a3..20e637a0a5 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { api(deps.kotlin.coroutines) api(deps.arrow.core) api(deps.arrow.fx) + api(projects.domain.models) implementation(deps.kotlin.serialization) diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt new file mode 100644 index 0000000000..fbcfeb9a0c --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt @@ -0,0 +1,145 @@ +package com.tangem.domain.core.wallets + +import arrow.core.Either +import com.tangem.domain.core.wallets.error.DeleteWalletError +import com.tangem.domain.core.wallets.error.LockWalletsError +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.core.wallets.error.SelectWalletError +import com.tangem.domain.core.wallets.error.SetLockError +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.StateFlow + +/** + * Repository for managing user wallets list. + * It provides methods to load, select, save, lock, unlock, and delete user wallets. + * + * TODO tests [REDACTED_TASK_KEY] + * + * @see com.tangem.domain.models.wallet.UserWallet + * @see com.tangem.domain.models.wallet.UserWalletId + */ +interface UserWalletsListRepository { + + /** + * List of user wallets. + * It can be null if the list is not loaded yet. + */ + val userWallets: StateFlow?> + + /** + * Currently selected user wallet. + * It can be null if wallets list is not loaded yet or wallets list is empty. + */ + val selectedUserWallet: StateFlow + + /** + * Loads user wallets list and selected wallet. + * If the list is already loaded, it does nothing. + */ + suspend fun load() + + /** + * Gets and if necessary loads user wallets list and selected wallet. + */ + suspend fun userWalletsSync(): List + + /** + * Gets and if necessary loads selected user wallet. + */ + suspend fun selectedUserWalletSync(): UserWallet? + + /** + * Selects user wallet by id. + * If the wallet is not found, it returns [SelectWalletError.UnableToSelectUserWallet]. + */ + suspend fun select(userWalletId: UserWalletId): Either + + /** + * Saves user wallet. + * If the wallet already exists and [canOverride] is false, it returns [SaveWalletError.WalletAlreadySaved]. + * If the wallet already exists and [canOverride] is true, it overrides the existing wallet. + * + * Does not lock the wallet after saving, it should be done manually using [setLock] method. + */ + suspend fun saveWithoutLock( + userWallet: UserWallet, + canOverride: Boolean = true, + ): Either + + /** + * Sets lock for **unlocked** user wallet. + * If the wallet is not found, it returns [SetLockError.UserWalletNotFound] + * If the wallet is locked, it returns [SetLockError.UserWalletLocked] + * If the lock method is not supported, it returns [SetLockError.UnableToSetLock]. + * + * @param userWalletId The ID of the user wallet to set the lock for. + * @param lockMethod The method to use for locking the wallet. + * @param changeUnsecured If false, the method will have no effect on unsecured wallets. + */ + suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean = true, + ): Either + + /** + * Removes biometric lock for user wallet if it is set. + */ + suspend fun removeBiometricLock(userWalletId: UserWalletId) + + /** + * Deletes user wallets by ids. + * If the wallet is not found, it returns [DeleteWalletError.UnableToDelete] + */ + suspend fun delete(userWalletIds: List): Either + + /** + * Unlocks specific user wallet. + * If the wallet is already unlocked, returns [UnlockWalletError.AlreadyUnlocked]. + * If the wallet is not found, returns [UnlockWalletError.UserWalletNotFound]. + * If the unlock method is not supported, returns [UnlockWalletError.UnableToUnlock] + * If the user cancels the unlock operation (ex. dismisses dialogs), returns [UnlockWalletError.UserCancelled]. + * If the scanned card does not match the wallet, returns [UnlockWalletError.ScannedCardWalletNotMatched]. + */ + suspend fun unlock(userWalletId: UserWalletId, unlockMethod: UnlockMethod): Either + + /** + * Unlocks all user wallets using biometric authentication. + * If all the wallets was are already unlocked, returns [UnlockWalletError.AlreadyUnlocked]. + * Success if at least one wallet was unlocked. + * If the biometric method is not supported for some of user wallets, returns [UnlockWalletError.UnableToUnlock] + */ + suspend fun unlockAllWallets(): Either + + /** + * Locks all secured user wallets (wallets that are not locked with [LockMethod.NoLock]). + * If all the wallets are already locked or unsecured, returns [LockWalletsError.NothingToLock]. + * Success if at least one wallet was locked. + */ + suspend fun lockAllWallets(): Either + + /** + * Clears all persistent data related to user wallets. + * This includes removing all user wallets, selected wallet, and any other related data. + * User wallets will stay in the cache, but will be reloaded on next repository initialization. + */ + suspend fun clearPersistentData() + + sealed class LockMethod { + data object Biometric : LockMethod() + class AccessCode(val accessCode: CharArray) : LockMethod() + data object NoLock : LockMethod() + } + + enum class UnlockMethod { + Biometric, + AccessCode, + Scan, + } +} + +fun UserWalletsListRepository.requireUserWalletsSync(): List { + return userWallets.value ?: error("User wallets list is not loaded") +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/DeleteWalletError.kt similarity index 66% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt rename to domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/DeleteWalletError.kt index d58c220ac3..91b685a84c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/DeleteWalletError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.wallets.models +package com.tangem.domain.core.wallets.error sealed interface DeleteWalletError { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt new file mode 100644 index 0000000000..abedc5aa4a --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.wallets.error + +interface LockWalletsError { + + data object NothingToLock : LockWalletsError +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt new file mode 100644 index 0000000000..24c29fd411 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SaveFirstColdWalletError { + data object CreateWalletError : SaveFirstColdWalletError + data class SaveError(val error: SaveWalletError) : SaveFirstColdWalletError + data class SelectError(val error: SelectWalletError) : SaveFirstColdWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveWalletError.kt similarity index 84% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt rename to domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveWalletError.kt index a41524b8b7..30fc8a743b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveWalletError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.wallets.models +package com.tangem.domain.core.wallets.error /** [REDACTED_AUTHOR] diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt new file mode 100644 index 0000000000..05b07e53d0 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SelectWalletError { + + data object UnableToSelectUserWallet : SelectWalletError +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt new file mode 100644 index 0000000000..688f618c74 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SetLockError { + + data object UserWalletNotFound : SetLockError + + data object UserWalletLocked : SetLockError + + data class UnableToSetLock(val cause: Throwable) : SetLockError +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt new file mode 100644 index 0000000000..8bed3120a7 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.core.wallets.error + +sealed interface UnlockWalletError { + + data object AlreadyUnlocked : UnlockWalletError + + data object UserWalletNotFound : UnlockWalletError + + data object UnableToUnlock : UnlockWalletError + + data object UserCancelled : UnlockWalletError + + data object ScannedCardWalletNotMatched : UnlockWalletError +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index f48f6aadaf..087b4312ed 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -3,9 +3,9 @@ package com.tangem.domain.exchange import arrow.core.Either import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.transaction.models.AssetRequirementsCondition import kotlinx.coroutines.flow.Flow diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index c60557ebe2..e5fffdace4 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.staking) implementation(projects.domain.tokens) implementation(projects.domain.card) + implementation(projects.domain.wallets) implementation(projects.domain.legacy) /* Core */ diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 1c78e3949d..6c1b21b89b 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.managetokens import arrow.core.Either import arrow.core.flatten -import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.models.currency.CryptoCurrency @@ -10,9 +9,11 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository @Suppress("LongParameterList") class SaveManagedTokensUseCase( @@ -23,6 +24,7 @@ class SaveManagedTokensUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke( @@ -91,11 +93,12 @@ class SaveManagedTokensUseCase( userWalletId: UserWalletId, addedCurrencies: List, ) { + val stakingIds = addedCurrencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = addedCurrencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt index 12ab5b67e1..01cff0386d 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt @@ -1,7 +1,7 @@ package com.tangem.domain.markets -import com.tangem.domain.core.serialization.SerializedBigDecimal import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable @Serializable diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt index 7d01fd1314..044bc6a726 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -4,13 +4,15 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.card.common.extensions.supportedBlockchains -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync class FilterAvailableNetworksForWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val excludedBlockchains: ExcludedBlockchains, ) { @@ -22,25 +24,23 @@ class FilterAvailableNetworksForWalletUseCase( userWalletId: UserWalletId, networks: Set, ): Set { - val userWallet = userWalletsListManager.userWalletsSync.firstOrNull { + val userWallet = getWallets().firstOrNull { it.walletId == userWalletId } ?: return networks.toSet() - return when (userWallet) { - is UserWallet.Cold -> { - val supportedBlockchains = userWallet.scanResponse.card.supportedBlockchains( - cardTypesResolver = userWallet.scanResponse.cardTypesResolver, - excludedBlockchains = excludedBlockchains, - ) + val supportedBlockchains = userWallet.supportedBlockchains( + excludedBlockchains = excludedBlockchains, + ) - networks.filter { - val blockchain = Blockchain.fromNetworkId(it.networkId) - supportedBlockchains.contains(blockchain) - }.toSet() - } - is UserWallet.Hot -> { - networks.toSet() // TODO [REDACTED_TASK_KEY] - } - } + return networks.filter { + val blockchain = Blockchain.fromNetworkId(it.networkId) + supportedBlockchains.contains(blockchain) + }.toSet() + } + + private fun getWallets() = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index d5bbb3f689..3417a876fd 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -1,13 +1,14 @@ package com.tangem.domain.markets import arrow.core.Either -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,6 +29,7 @@ class SaveMarketTokensUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke( @@ -89,11 +91,12 @@ class SaveMarketTokensUseCase( userWalletId: UserWalletId, existingCurrencies: List, ) { + val stakingIds = existingCurrencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index 435a46452f..6c9f6b1143 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(tangemDeps.hot.core) implementation(deps.moshi.kotlin) implementation(deps.moshi.adapters) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) ksp(deps.moshi.kotlin.codegen) implementation(deps.arrow.core) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt index 73edfce845..641ec0eaa7 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt @@ -1,10 +1,13 @@ package com.tangem.domain.models +import kotlinx.serialization.Serializable + /** * Source of the status of any loaded data * [REDACTED_AUTHOR] */ +@Serializable enum class StatusSource { /** diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt index e7b9ccdc03..374956a548 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt @@ -1,22 +1,26 @@ package com.tangem.domain.models -import java.math.BigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable /** * Represents the possible states of the fiat balance, including loading, failure, or a loaded amount */ +@Serializable sealed interface TotalFiatBalance { /** * Represents the loading state of the fiat balance. * This state indicates that the fiat balance is currently being retrieved or calculated. */ + @Serializable data object Loading : TotalFiatBalance /** * Represents the failure state of the fiat balance. * This state indicates that an attempt to retrieve or calculate the fiat balance has failed. */ + @Serializable data object Failed : TotalFiatBalance /** @@ -25,8 +29,9 @@ sealed interface TotalFiatBalance { * @property amount the loaded fiat balance amount * @property isAllAmountsSummarized indicates whether the amount includes a summary of all underlying amounts */ + @Serializable data class Loaded( - val amount: BigDecimal, + val amount: SerializedBigDecimal, val isAllAmountsSummarized: Boolean, val source: StatusSource, ) : TotalFiatBalance diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt new file mode 100644 index 0000000000..db4a5b164c --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -0,0 +1,198 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +/** + * Represents an account + * +[REDACTED_AUTHOR] + */ +@Serializable +sealed interface Account { + + /** Unique identifier of the account */ + val accountId: AccountId + + /** Name of the account */ + val name: AccountName + + /** The identifier of the user wallet associated with the account */ + val userWalletId: UserWalletId + get() = accountId.userWalletId + + /** + * Represents a crypto portfolio account + * + * @property accountId unique identifier of the account + * @property name name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property isArchived indicates whether the account is archived + * @property cryptoCurrencyList list of tokens associated with the account + */ + @Serializable + data class CryptoPortfolio private constructor( + override val accountId: AccountId, + override val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: DerivationIndex, + val isArchived: Boolean, + val cryptoCurrencyList: CryptoCurrencyList, + ) : Account { + + /** Indicates if the account is the main account */ + val isMainAccount: Boolean + get() = derivationIndex.isMain + + /** Number of tokens in the account */ + val tokensCount: Int + get() = cryptoCurrencyList.currencies.size + + /** Number of distinct networks in the account */ + val networksCount: Int + get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + + fun copy( + accountName: AccountName = this.name, + accountIcon: CryptoPortfolioIcon = this.icon, + isArchived: Boolean = this.isArchived, + ): CryptoPortfolio { + return CryptoPortfolio( + accountId = this.accountId, + name = accountName, + icon = accountIcon, + derivationIndex = this.derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = this.cryptoCurrencyList, + ) + } + + /** + * Represents a list of tokens in the account + * + * @property currencies set of cryptocurrencies in the account + * @property sortType sorting type for the tokens + * @property groupType grouping type for the tokens + */ + @Serializable + data class CryptoCurrencyList( + val currencies: Set, + val sortType: TokensSortType, + val groupType: TokensGroupType, + ) + + /** + * Represents possible errors when creating a crypto portfolio account + */ + @Serializable + sealed interface Error { + + /** Error indicating that the account name is blank */ + @Serializable + data class AccountNameError(val cause: AccountName.Error) : Error + + /** Error indicating that the derivation index is negative */ + @Serializable + data class DerivationIndexError(val cause: DerivationIndex.Error) : Error + } + + companion object { + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param name name of the account + * @param accountIcon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param isArchived indicates whether the account is archived + * @param cryptoCurrencyList list of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + name: String, + accountIcon: CryptoPortfolioIcon, + derivationIndex: Int, + isArchived: Boolean, + cryptoCurrencyList: CryptoCurrencyList, + ): Either { + return either { + val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind() + val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind() + + invoke( + accountId = accountId, + accountName = accountName, + accountIcon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) + } + } + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param accountIcon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param isArchived indicates whether the account is archived + * @param cryptoCurrencyList list of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + accountName: AccountName, + accountIcon: CryptoPortfolioIcon, + derivationIndex: DerivationIndex, + isArchived: Boolean, + cryptoCurrencyList: CryptoCurrencyList, + ): CryptoPortfolio { + return CryptoPortfolio( + accountId = accountId, + name = accountName, + icon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) + } + + /** + * Creates a main account for the given user wallet ID + * + * @param userWalletId the ID of the user wallet + */ + fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { + val derivationIndex = DerivationIndex.Main + + return CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + name = AccountName.Main, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt new file mode 100644 index 0000000000..a9cf874078 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.models.account + +import com.tangem.common.extensions.toByteArray +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.extensions.toHexString +import kotlinx.serialization.Serializable +import java.security.MessageDigest + +/** + * Represents a unique identifier for an account + * + * @property value a unique string value that distinguishes this account + * @property userWalletId the identifier of the user wallet associated with the account + */ +@Serializable +data class AccountId private constructor( + val value: String, + val userWalletId: UserWalletId, +) { + + companion object { + + private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") } + + /** + * Creates a unique account identifier for a crypto portfolio + * + * @param userWalletId the identifier of the user wallet + * @param derivationIndex the derivation index used to generate the identifier + */ + fun forCryptoPortfolio(userWalletId: UserWalletId, derivationIndex: DerivationIndex): AccountId { + val input = userWalletId.value + derivationIndex.value.toByteArray() + val value = sha256Digest.digest(input).toHexString() + + return AccountId(value = value, userWalletId = userWalletId) + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt new file mode 100644 index 0000000000..29fe653a8b --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt @@ -0,0 +1,69 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import kotlinx.serialization.Serializable + +/** + * Represents an account name + * + * @property value the validated account name as a string + * +[REDACTED_AUTHOR] + */ +@Serializable +data class AccountName private constructor( + val value: String, +) { + + /** + * Represents possible validation errors + */ + @Serializable + sealed interface Error { + + /** + * Error indicating that the account name is blank + */ + @Serializable + data object Empty : Error { + override fun toString(): String = "${Empty::class.simpleName}: Account name cannot be blank" + } + + /** + * Error indicating that the account name exceeds the maximum allowed length + */ + @Serializable + data object ExceedsMaxLength : Error { + override fun toString(): String { + return "${ExceedsMaxLength::class.simpleName}: Account name cannot exceed $MAX_LENGTH characters" + } + } + } + + companion object { + + private const val MAIN_ACCOUNT_NAME = "Main Account" + private const val MAX_LENGTH = 20 + + /** Default name for the main account */ + val Main: AccountName + get() = AccountName(value = MAIN_ACCOUNT_NAME) + + /** + * Factory method to create an `AccountName` instance. + * Validates the input string to ensure it is not blank and does not exceed the maximum length. + * + * @param value the input string representing the account name + */ + operator fun invoke(value: String): Either = either { + val trimmedValue = value.trim() + + ensure(trimmedValue.isNotBlank()) { Error.Empty } + ensure(trimmedValue.length <= MAX_LENGTH) { Error.ExceedsMaxLength } + + AccountName(value = trimmedValue) + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt new file mode 100644 index 0000000000..73bc34e70e --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.tokenlist.TokenList +import kotlinx.serialization.Serializable + +/** + * Represents the status of an account + * +[REDACTED_AUTHOR] + */ +@Serializable +sealed interface AccountStatus { + + /** The account associated with this status */ + val account: Account + + /** + * Represents the status of a crypto portfolio account + * + * @property account the crypto portfolio account + * @property tokenList the list of tokens associated with the account + */ + @Serializable + data class CryptoPortfolio( + override val account: Account.CryptoPortfolio, + val tokenList: TokenList, + ) : AccountStatus +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt index 9846695785..15b906020b 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt @@ -1,48 +1,26 @@ package com.tangem.domain.models.account -import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofCustomAccount +import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofDefaultCustomAccount import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofMainAccount +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable /** * Represents an icon for an [Account.CryptoPortfolio] account * - * @property type the type of the account icon + * @property value the type of the account icon * @property color the color of the account icon * - * @constructor [ofMainAccount], [ofCustomAccount] + * @constructor [ofMainAccount], [ofDefaultCustomAccount] * [REDACTED_AUTHOR] */ @Serializable data class CryptoPortfolioIcon private constructor( - val type: Type, + val value: Icon, val color: Color, ) { - /** - * Represents the type of an account icon. Can either be a specific [Icon] or a [Symbol] - */ - @Serializable - sealed interface Type { - - /** - * Represents a specific predefined icon type - * - * @property value the predefined [Icon] of the icon - */ - @Serializable - data class Icon(val value: CryptoPortfolioIcon.Icon) : Type - - /** - * Represents an icon with a letter - * - * @property value the letter used as the icon - */ - @Serializable - data class Symbol(val value: Char) : Type - } - /** * Enum class representing the icons of accounts */ @@ -91,59 +69,44 @@ data class CryptoPortfolioIcon private constructor( companion object { - private val defaultMainAccountType: Icon = Icon.Star - private val defaultMainAccountColor: Color = Color.Azure + private val defaultMainAccountIcon: Icon = Icon.Star + private val excludedCustomAccountIcons: Set = setOf(Icon.Letter, Icon.Star) + private const val HASH_MULTIPLIER = 31 /** - * Creates an [CryptoPortfolioIcon] for the Main account, ensuring the color is not in the excluded set. + * Creating a [CryptoPortfolioIcon] for the Main account with default values. + * The color is derived from the [UserWalletId]. * - * @param exclude excluded colors that are already used for main accounts + * @param userWalletId the ID of the user wallet */ - fun ofMainAccount(exclude: Set): CryptoPortfolioIcon { - val isDefaultColorBusy = defaultMainAccountColor in exclude + fun ofMainAccount(userWalletId: UserWalletId): CryptoPortfolioIcon { + val colors = Color.entries + val hash = userWalletId.value.fold(0) { acc, byte -> acc * HASH_MULTIPLIER + byte } - val color = if (isDefaultColorBusy) { - val colorsWithExcluded = Color.entries - exclude + val index = (hash and Int.MAX_VALUE) % colors.size + val color = colors[index] - val availableColors = if (colorsWithExcluded.isNotEmpty()) { - colorsWithExcluded - } else { - Color.entries - } - - availableColors.random() - } else { - defaultMainAccountColor - } - - return CryptoPortfolioIcon( - type = Type.Icon(value = defaultMainAccountType), - color = color, - ) + return CryptoPortfolioIcon(value = defaultMainAccountIcon, color = color) } /** * Creates an [CryptoPortfolioIcon] for a user account based on the account name - * - * @param accountName the name of the account, used to determine the letter for the icon */ - fun ofCustomAccount(accountName: String): CryptoPortfolioIcon { + fun ofDefaultCustomAccount(): CryptoPortfolioIcon { + val icon = (Icon.entries - excludedCustomAccountIcons).random() val color = Color.entries.random() - return CryptoPortfolioIcon( - type = Type.Symbol(value = accountName.first()), - color = color, - ) + return CryptoPortfolioIcon(value = icon, color = color) } /** * Creates a [CryptoPortfolioIcon] for a user account with a specific type and color * - * @param type the type of the account icon + * @param value the icon of the account * @param color the color of the account icon */ - fun ofCustomAccount(type: Type, color: Color): CryptoPortfolioIcon { - return CryptoPortfolioIcon(type = type, color = color) + fun ofCustomAccount(value: Icon, color: Color): CryptoPortfolioIcon { + return CryptoPortfolioIcon(value = value, color = color) } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt new file mode 100644 index 0000000000..5ffa4e2ecc --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import kotlinx.serialization.Serializable + +/** + * Represents a derivation index for accounts, ensuring validity and providing utility methods + * + * @property value the integer value of the derivation index + * +[REDACTED_AUTHOR] + */ +@Serializable +data class DerivationIndex private constructor( + val value: Int, +) { + + /** Checks if the derivation index corresponds to the main account */ + val isMain: Boolean + get() = value == MAIN_ACCOUNT_DERIVATION_INDEX + + /** + * Represents possible errors that can occur when creating a [DerivationIndex] + */ + @Serializable + sealed interface Error { + + /** Error indicating that the provided derivation index [derivationIndex] is invalid */ + @Serializable + data class NegativeDerivationIndex(val derivationIndex: Int) : Error { + override fun toString(): String { + return "${this::class.simpleName}: Derivation index cannot be negative: $derivationIndex" + } + } + } + + companion object { + + private const val MAIN_ACCOUNT_DERIVATION_INDEX = 0 + + /** Predefined instance of [DerivationIndex] for the main account */ + val Main: DerivationIndex = DerivationIndex(value = MAIN_ACCOUNT_DERIVATION_INDEX) + + /** + * Factory method to create a [DerivationIndex] instance + * + * @param value the integer value of the derivation index + * + * @return Either an error if the value is invalid, or a valid [DerivationIndex] instance + */ + operator fun invoke(value: Int): Either = either { + ensure(value >= 0) { Error.NegativeDerivationIndex(derivationIndex = value) } + + DerivationIndex(value) + } + } +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt similarity index 68% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt index 65d534c54b..64e478276e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt @@ -1,12 +1,12 @@ -package com.tangem.domain.tokens.model +package com.tangem.domain.models.currency import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.staking.model.stakekit.YieldBalance -import java.math.BigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import com.tangem.domain.models.staking.YieldBalance +import kotlinx.serialization.Serializable /** * Represents the status of a cryptocurrency asset within a network. @@ -18,6 +18,7 @@ import java.math.BigDecimal * @property currency The details of the cryptocurrency asset, including its type, name, symbol, and other properties. * @property value The current status of the cryptocurrency, reflecting its state within the network. */ +@Serializable data class CryptoCurrencyStatus( val currency: CryptoCurrency, val value: Value, @@ -28,36 +29,40 @@ data class CryptoCurrencyStatus( * * @property isError Indicates whether this status represents an error status. */ - sealed class Value(val isError: Boolean) { + @Serializable + sealed interface Value { + + val isError: Boolean /** The amount of the cryptocurrency. */ - open val amount: BigDecimal? = null + val amount: SerializedBigDecimal? get() = null /** The fiat equivalent of the cryptocurrency's amount. */ - open val fiatAmount: BigDecimal? = null + val fiatAmount: SerializedBigDecimal? get() = null /** The exchange rate used for converting the cryptocurrency amount to fiat. */ - open val fiatRate: BigDecimal? = null + val fiatRate: SerializedBigDecimal? get() = null /** The change in price of the cryptocurrency. */ - open val priceChange: BigDecimal? = null + val priceChange: SerializedBigDecimal? get() = null /** Indicates if there are any transactions in progress related to the cryptocurrency network. */ - open val hasCurrentNetworkTransactions: Boolean = false + val hasCurrentNetworkTransactions: Boolean get() = false /** The pending cryptocurrency transactions. */ - open val pendingTransactions: Set = emptySet() + val pendingTransactions: Set get() = emptySet() /** The network address */ - open val networkAddress: NetworkAddress? = null + val networkAddress: NetworkAddress? get() = null /** Staking yield balance */ - open val yieldBalance: YieldBalance? = null + val yieldBalance: YieldBalance? get() = null /** Sources */ - open val sources: Sources = Sources() + val sources: Sources get() = Sources() } + @Serializable data class Sources( val networkSource: StatusSource = StatusSource.ACTUAL, val quoteSource: StatusSource = StatusSource.ACTUAL, @@ -70,7 +75,11 @@ data class CryptoCurrencyStatus( } /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ - data object Loading : Value(isError = false) + @Serializable + data object Loading : Value { + + override val isError: Boolean = false + } /** * Represents a state where the cryptocurrency is not reachable. @@ -79,23 +88,35 @@ data class CryptoCurrencyStatus( * @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat. * @property networkAddress The network address */ + @Serializable data class Unreachable( - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, override val networkAddress: NetworkAddress?, - ) : Value(isError = true) + ) : Value { + + override val isError: Boolean = true + } /** Represents a state where the cryptocurrency's network amount not found. */ + @Serializable data class NoAmount( - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, - ) : Value(isError = true) + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, + ) : Value { + + override val isError: Boolean = true + } /** Represents a state where the cryptocurrency's derivation is missed. */ + @Serializable data class MissedDerivation( - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, - ) : Value(isError = true) + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, + ) : Value { + + override val isError: Boolean = true + } /** * Represents a state where there is no account associated with the cryptocurrency @@ -103,16 +124,18 @@ data class CryptoCurrencyStatus( * @property amountToCreateAccount base reserve amount for account creation * @property sources sources of data */ + @Serializable data class NoAccount( - val amountToCreateAccount: BigDecimal, - override val fiatAmount: BigDecimal?, - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, + val amountToCreateAccount: SerializedBigDecimal, + override val fiatAmount: SerializedBigDecimal?, + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) { + ) : Value { - override val amount: BigDecimal = BigDecimal.ZERO + override val isError: Boolean = false + override val amount: SerializedBigDecimal? = SerializedBigDecimal.ZERO } /** @@ -127,17 +150,21 @@ data class CryptoCurrencyStatus( * @property pendingTransactions The current cryptocurrency transactions. * @property sources sources of data */ + @Serializable data class Loaded( - override val amount: BigDecimal, - override val fiatAmount: BigDecimal, - override val fiatRate: BigDecimal, - override val priceChange: BigDecimal, + override val amount: SerializedBigDecimal, + override val fiatAmount: SerializedBigDecimal, + override val fiatRate: SerializedBigDecimal, + override val priceChange: SerializedBigDecimal, override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) + ) : Value { + + override val isError: Boolean = false + } /** * Represents a Custom state of a cryptocurrency, typically used for user-defined tokens. @@ -150,17 +177,21 @@ data class CryptoCurrencyStatus( * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. */ + @Serializable data class Custom( - override val amount: BigDecimal, - override val fiatAmount: BigDecimal?, - override val fiatRate: BigDecimal?, - override val priceChange: BigDecimal?, + override val amount: SerializedBigDecimal, + override val fiatAmount: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, + override val priceChange: SerializedBigDecimal?, override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) + ) : Value { + + override val isError: Boolean = false + } /** * Represents a state where the cryptocurrency is available, but there is no current quote available for it. @@ -170,12 +201,16 @@ data class CryptoCurrencyStatus( * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. */ + @Serializable data class NoQuote( - override val amount: BigDecimal, + override val amount: SerializedBigDecimal, override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) + ) : Value { + + override val isError: Boolean = false + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt index 95d2ea6ad0..a955b53e54 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt @@ -20,6 +20,7 @@ import kotlinx.serialization.Serializable * currency (for those blockchains that have FeeResource instead of a standard type of fee) * @property canHandleTokens indicates whether the network can handle tokens * @property transactionExtrasType the type of extras supported for sending a transaction + * @property nameResolvingType the type of on-chain name resolution supported by the network (e.g., ENS, SNS etc) */ @Serializable data class Network( @@ -33,6 +34,7 @@ data class Network( val hasFiatFeeRate: Boolean, val canHandleTokens: Boolean, val transactionExtrasType: TransactionExtrasType, + val nameResolvingType: NameResolvingType, ) { /** Raw ID */ @@ -161,4 +163,16 @@ data class Network( -> true } } + + /** + * Represents the type of on-chain name resolution supported by the network. + */ + enum class NameResolvingType { + + /** No name resolution supported */ + NONE, + + /** Ethereum Name Service (ENS) */ + ENS, + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt index 0fef921e21..f32f27b752 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt @@ -1,6 +1,9 @@ package com.tangem.domain.models.network +import kotlinx.serialization.Serializable + /** Represents a network address */ +@Serializable sealed class NetworkAddress { /** The default or currently selected network address */ @@ -14,6 +17,7 @@ sealed class NetworkAddress { * * @property defaultAddress the static network address */ + @Serializable data class Single(override val defaultAddress: Address) : NetworkAddress() { override val availableAddresses: Set
= setOf(defaultAddress) @@ -25,6 +29,7 @@ sealed class NetworkAddress { * @property defaultAddress the currently selected or default network address * @property availableAddresses the set of available network addresses to choose from */ + @Serializable data class Selectable( override val defaultAddress: Address, override val availableAddresses: Set
, @@ -41,6 +46,7 @@ sealed class NetworkAddress { * @property value string representation of the address * @property type address type */ + @Serializable data class Address( val value: String, val type: Type, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt index f2d47d0f00..d5bb077099 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt @@ -1,6 +1,7 @@ package com.tangem.domain.models.network -import java.math.BigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable /** * Represents information about a transaction. Do not use it for sending transactions. @@ -15,6 +16,7 @@ import java.math.BigDecimal * @property type transaction type * @property amount transaction amount */ +@Serializable data class TxInfo( val txHash: String, val timestampInMillis: Long, @@ -24,10 +26,11 @@ data class TxInfo( val interactionAddressType: InteractionAddressType?, val status: TransactionStatus, val type: TransactionType, - val amount: BigDecimal, + val amount: SerializedBigDecimal, ) { /** Destination type*/ + @Serializable sealed class DestinationType { /** @@ -35,6 +38,7 @@ data class TxInfo( * * @property addressType address type */ + @Serializable data class Single(val addressType: AddressType) : DestinationType() /** @@ -42,21 +46,29 @@ data class TxInfo( * * @property addressTypes addresses types */ + @Serializable data class Multiple(val addressTypes: List) : DestinationType() } /** Address type */ + @Serializable sealed class AddressType { /** Address value */ abstract val address: String + @Serializable data class User(override val address: String) : AddressType() + + @Serializable data class Contract(override val address: String) : AddressType() + + @Serializable data class Validator(override val address: String) : AddressType() } /** Source type */ + @Serializable sealed class SourceType { /** @@ -64,6 +76,7 @@ data class TxInfo( * * @property address address */ + @Serializable data class Single(val address: String) : SourceType() /** @@ -71,38 +84,79 @@ data class TxInfo( * * @property addresses addresses */ + @Serializable data class Multiple(val addresses: List) : SourceType() } /** Transaction type */ + @Serializable sealed interface TransactionType { + + @Serializable data object Transfer : TransactionType + + @Serializable data object Approve : TransactionType + + @Serializable data object Swap : TransactionType + + @Serializable data object UnknownOperation : TransactionType + + @Serializable data class Operation(val name: String) : TransactionType + @Serializable sealed interface Staking : TransactionType { + + @Serializable data class Vote(val validatorAddress: String) : Staking + + @Serializable data object ClaimRewards : Staking + + @Serializable data object Stake : Staking + + @Serializable data object Unstake : Staking + + @Serializable data object Withdraw : Staking + + @Serializable data object Restake : Staking } } /** Transaction status */ + @Serializable sealed class TransactionStatus { + + @Serializable data object Failed : TransactionStatus() + + @Serializable data object Unconfirmed : TransactionStatus() + + @Serializable data object Confirmed : TransactionStatus() } + @Serializable sealed class InteractionAddressType { + + @Serializable data class Validator(val address: String) : InteractionAddressType() + + @Serializable data class User(val address: String) : InteractionAddressType() + + @Serializable data class Contract(val address: String) : InteractionAddressType() + + @Serializable data class Multiple(val addresses: List) : InteractionAddressType() } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigDecimalSerializer.kt similarity index 93% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigDecimalSerializer.kt index 639de2993b..af3fb7323d 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigDecimalSerializer.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigIntegerSerializer.kt similarity index 93% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigIntegerSerializer.kt index 93394b1e0c..f9be82fe0b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigIntegerSerializer.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigDecimal.kt similarity index 77% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigDecimal.kt index 3243892b18..c75fe1644b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigDecimal.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.Serializable import java.math.BigDecimal diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigInteger.kt similarity index 77% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigInteger.kt index 8fbbda7eea..d5a6847870 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigInteger.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.Serializable import java.math.BigInteger diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt similarity index 90% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt index 2733a30688..3e0921c351 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt @@ -1,5 +1,8 @@ -package com.tangem.domain.staking.model.stakekit +package com.tangem.domain.models.staking +import kotlinx.serialization.Serializable + +@Serializable enum class NetworkType { AVALANCHE_C, AVALANCHE_ATOMIC, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt new file mode 100644 index 0000000000..5ed3a0b3ce --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.models.staking + +import kotlinx.serialization.Serializable + +@Serializable +data class StakingID(val integrationId: String, val address: String) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalance.kt new file mode 100644 index 0000000000..3badeed6e2 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalance.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.models.staking + +import com.tangem.domain.models.StatusSource +import kotlinx.serialization.Serializable + +/** + * Represents a yield balance in the staking system + */ +@Serializable +sealed interface YieldBalance { + + /** The unique identifier of the staking operation */ + val stakingId: StakingID + + /** The source of the status information */ + val source: StatusSource + + /** + * Represents a yield balance with actual data + * + * @property stakingId the unique identifier of the staking operation + * @property source the source of the status information + * @property balance the balance details of the yield + */ + @Serializable + data class Data( + override val stakingId: StakingID, + override val source: StatusSource, + val balance: YieldBalanceItem, + ) : YieldBalance + + /** + * Represents an empty yield balance + * + * @property stakingId the unique identifier of the staking operation + * @property source the source of the status information + */ + @Serializable + data class Empty( + override val stakingId: StakingID, + override val source: StatusSource, + ) : YieldBalance + + /** + * Represents an error state for the yield balance + * + * @property stakingId the unique identifier of the staking operation + */ + @Serializable + data class Error(override val stakingId: StakingID) : YieldBalance { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Creates a copy of the current yield balance with a new status source + * + * @param source the new source of the status information + */ + fun copySealed(source: StatusSource): YieldBalance { + return when (this) { + is Data -> copy(source = source) + is Empty -> copy(source = source) + is Error, + -> this + } + } +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt similarity index 55% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt index 44e0681f52..f5aef4acfa 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt @@ -1,88 +1,44 @@ -package com.tangem.domain.staking.model.stakekit +package com.tangem.domain.models.staking -import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import org.joda.time.DateTime -import java.math.BigDecimal - -sealed class YieldBalance { - - abstract val integrationId: String? - abstract val address: String? - abstract val source: StatusSource - - fun copySealed(source: StatusSource): YieldBalance { - return when (this) { - is Data -> copy(source = source) - is Empty -> copy(source = source) - is Error, - is Unsupported, - -> this - } - } - - fun getStakingId(): StakingID? { - val integrationId = integrationId - val address = address - - if (integrationId == null || address == null) return null - - return StakingID(integrationId = integrationId, address = address) - } - - data class Data( - override val integrationId: String?, - override val address: String, - override val source: StatusSource, - val balance: YieldBalanceItem, - ) : YieldBalance() - - data class Empty( - override val integrationId: String?, - override val address: String, - override val source: StatusSource, - ) : YieldBalance() - - data object Unsupported : YieldBalance() { - override val integrationId: String? = null - override val address: String? = null - override val source: StatusSource = StatusSource.ACTUAL - } - - data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() { - override val source: StatusSource = StatusSource.ACTUAL - } -} +import com.tangem.domain.models.serialization.SerializedBigDecimal +import com.tangem.domain.models.staking.action.StakingActionType +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +@Serializable data class YieldBalanceItem( val items: List, - val integrationId: String?, + val integrationId: String, ) +@Serializable data class BalanceItem( val groupId: String, - val token: Token, + val token: YieldToken, val type: BalanceType, - val amount: BigDecimal, + val amount: SerializedBigDecimal, val rawCurrencyId: String?, val validatorAddress: String?, - val date: DateTime?, + val date: Instant?, val pendingActions: List, val pendingActionsConstraints: List, val isPending: Boolean, ) +@Serializable data class PendingActionConstraints( val type: StakingActionType, val amountArg: PendingAction.PendingActionArgs.Amount?, ) +@Serializable data class PendingAction( val type: StakingActionType, val passthrough: String, val args: PendingActionArgs?, ) { + + @Serializable data class PendingActionArgs( val amount: Amount?, val duration: Duration?, @@ -91,18 +47,22 @@ data class PendingAction( val tronResource: TronResource?, val signatureVerification: Boolean?, ) { + + @Serializable data class Amount( val required: Boolean, - val minimum: BigDecimal?, - val maximum: BigDecimal?, + val minimum: SerializedBigDecimal?, + val maximum: SerializedBigDecimal?, ) + @Serializable data class Duration( val required: Boolean, val minimum: Int?, val maximum: Int?, ) + @Serializable data class TronResource( val required: Boolean, val options: List, @@ -114,6 +74,7 @@ data class PendingAction( * IMPORTANT!!! * Order is used to sort balances. */ +@Serializable @Suppress("MagicNumber") enum class BalanceType(val order: Int) { AVAILABLE(1), @@ -128,6 +89,7 @@ enum class BalanceType(val order: Int) { ; companion object { + fun BalanceType.isClickable() = when (this) { STAKED, UNSTAKED, @@ -144,6 +106,7 @@ enum class BalanceType(val order: Int) { } } +@Serializable enum class RewardBlockType { NoRewards, Rewards, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt new file mode 100644 index 0000000000..18954a5c1f --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.models.staking + +import kotlinx.serialization.Serializable + +@Serializable +data class YieldToken( + val name: String, + val network: NetworkType, + val symbol: String, + val decimals: Int, + val address: String?, + val coinGeckoId: String?, + val logoURI: String?, + val isPoints: Boolean?, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/action/StakingActionType.kt similarity index 91% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/action/StakingActionType.kt index 467bb051aa..d255b23852 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/action/StakingActionType.kt @@ -1,5 +1,8 @@ -package com.tangem.domain.staking.model.stakekit.action +package com.tangem.domain.models.staking.action +import kotlinx.serialization.Serializable + +@Serializable enum class StakingActionType { STAKE, UNSTAKE, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt new file mode 100644 index 0000000000..bec343c4df --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt @@ -0,0 +1,88 @@ +package com.tangem.domain.models.tokenlist + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +/** + * Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped. + * + * The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection. + * Additional details like the total fiat balance and the sorting type can be associated with the list. + */ +@Serializable +sealed interface TokenList { + + /** The total fiat balance across all tokens */ + val totalFiatBalance: TotalFiatBalance + + /** The criteria used for sorting the tokens */ + val sortedBy: TokensSortType + + /** + * Represents tokens that are grouped by their network + * + * @property totalFiatBalance the total fiat balance across all groups + * @property sortedBy the criteria used for sorting the tokens within the groups + * @property groups a list of network groups containing tokens + */ + @Serializable + data class GroupedByNetwork( + override val totalFiatBalance: TotalFiatBalance, + override val sortedBy: TokensSortType, + val groups: List, + ) : TokenList { + + /** + * Represents a group of cryptocurrencies associated with a specific network + * + * @property network the blockchain network associated with the group + * @property currencies a list of cryptocurrency statuses that belong to the network + */ + @Serializable + data class NetworkGroup( + val network: Network, + val currencies: List, + ) + } + + /** + * Represents tokens that are not grouped by any specific criteria. + * + * @property totalFiatBalance the total fiat balance across all groups + * @property sortedBy the criteria used for sorting the tokens within the groups + * @property currencies a list of cryptocurrency statuses + */ + @Serializable + data class Ungrouped( + override val totalFiatBalance: TotalFiatBalance, + override val sortedBy: TokensSortType, + val currencies: List, + ) : TokenList + + /** Represents a state where the token list is empty */ + @Serializable + data object Empty : TokenList { + + override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( + amount = SerializedBigDecimal.ZERO, + isAllAmountsSummarized = true, + source = StatusSource.ACTUAL, + ) + + override val sortedBy: TokensSortType = TokensSortType.NONE + } + + /** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */ + fun flattenCurrencies(): List { + return when (this) { + is GroupedByNetwork -> groups.flatMap(GroupedByNetwork.NetworkGroup::currencies) + is Ungrouped -> currencies + is Empty -> emptyList() + } + } +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt new file mode 100644 index 0000000000..e988ec9fc1 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.models.account + +import com.google.common.truth.Truth +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountIdTest { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun forCryptoPortfolio(model: ForCryptoPortfolioModel) { + // Arrange + val userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F") + + // Act + val actual = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = model.derivationIndex) + + // Assert + Truth.assertThat(actual.value).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + ForCryptoPortfolioModel( + derivationIndex = DerivationIndex.Main, + expected = "4E39B13EA11E3B35339664A10BEF48F4AF752A1CC2200F79D23CB0FB3396C63F", + ), + ForCryptoPortfolioModel( + derivationIndex = DerivationIndex(1).getOrNull()!!, + expected = "7F22E71F8106783F0F2DAFCDE525E2F2A2281E864DDBE2FE668FA09329D563A2", + ), + ForCryptoPortfolioModel( + derivationIndex = DerivationIndex(42).getOrNull()!!, + expected = "555C1E17A302659446C97393453B7C2B3246AF4DA082C56C28FB6EDD1A6606A4", + ), + ) + + data class ForCryptoPortfolioModel( + val derivationIndex: DerivationIndex, + val expected: String, + ) +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt new file mode 100644 index 0000000000..e2aee138be --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt @@ -0,0 +1,78 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.left +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountNameTest { + + @Test + fun main_returnsMainAccountName() { + // Act + val main = AccountName.Main.value + + // Assert + val expected = "Main Account" + Truth.assertThat(main).isEqualTo(expected) + } + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: InvokeTestModel) { + // Act + val actual = AccountName(value = model.value) + + // Assert + actual + .onRight { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onLeft { + val expected = model.expected.leftOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + } + + private fun provideTestModels() = listOf( + InvokeTestModel( + value = "", + expected = AccountName.Error.Empty.left(), + ), + InvokeTestModel( + value = " ", + expected = AccountName.Error.Empty.left(), + ), + InvokeTestModel( + value = "a".repeat(21), + expected = AccountName.Error.ExceedsMaxLength.left(), + ), + "a".repeat(20).let { value -> + InvokeTestModel( + value = value, + expected = AccountName(value = value), + ) + }, + InvokeTestModel( + value = " name ", + expected = AccountName(value = "name"), + ), + InvokeTestModel( + value = "Main Account", + expected = AccountName(value = "Main Account"), + ), + ) + + data class InvokeTestModel( + val value: String, + val expected: Either, + ) +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt new file mode 100644 index 0000000000..eabb287672 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -0,0 +1,196 @@ +package com.tangem.domain.models.account + +import com.google.common.truth.Truth +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio +import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountTest { + + @Test + fun `Account userWalletId`() { + // Arrange + val userWalletId = UserWalletId("011") + + // Act + val actual = createCryptoPortfolioStub(userWalletId = userWalletId).userWalletId + + // Assert + Truth.assertThat(actual).isEqualTo(userWalletId) + } + + @Test + fun `CryptoPortfolio isMainAccount`() { + // Arrange + val derivationIndex0 = 0 + val derivationIndex1 = 1 + + // Act + val actual1 = createCryptoPortfolioStub(derivationIndex = derivationIndex0) + .isMainAccount + + val actual2 = createCryptoPortfolioStub(derivationIndex = derivationIndex1) + .isMainAccount + + // Assert + Truth.assertThat(actual1).isTrue() + Truth.assertThat(actual2).isFalse() + } + + @Test + fun `CryptoPortfolio tokensCount`() { + // Arrange + val emptyCurrencies = emptySet() + val filledCurrencies = setOf(mockk()) + + // Act + val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) + .tokensCount + + val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies) + .tokensCount + + // Assert + Truth.assertThat(actual1).isEqualTo(0) + Truth.assertThat(actual2).isEqualTo(1) + } + + @Test + fun `CryptoPortfolio networksCount`() { + // Arrange + val emptyCurrencies = emptySet() + val filledCurrencies = setOf( + mockk { + every { network } returns mockk() + }, + ) + + // Act + val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) + .networksCount + + val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies) + .networksCount + + // Assert + Truth.assertThat(actual1).isEqualTo(0) + Truth.assertThat(actual2).isEqualTo(1) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateCryptoPortfolio { + + @Test + fun `invoke returns AccountNameError`() { + // Arrange + val name = "" + + // Act + val actual = CryptoPortfolio( + accountId = mockk(), + name = name, + accountIcon = mockk(), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = mockk(), + ) + .leftOrNull()!! + + // Assert + val expected = AccountNameError(cause = AccountName.Error.Empty) + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `invoke returns CryptoPortfolio`() { + // Act + val derivationIndex = DerivationIndex.Main + val actual = CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio( + userWalletId = UserWalletId("011"), + derivationIndex = derivationIndex, + ), + name = "Test Account", + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + derivationIndex = derivationIndex.value, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + + // Assert + val expected = createCryptoPortfolioStub() + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun createMainAccount() { + // Arrange + val userWalletId = UserWalletId("011") + val derivationIndex = DerivationIndex.Main + + // Act + val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + // Assert + val expected = CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + accountName = AccountName.Main, + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + private fun createCryptoPortfolioStub( + userWalletId: UserWalletId = UserWalletId("011"), + name: String = "Test Account", + derivationIndex: Int = 0, + currencies: Set = emptySet(), + ): CryptoPortfolio { + val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!! + + return CryptoPortfolio.invoke( + accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex), + name = name, + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = currencies, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + } +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt index 3706e6921d..ec55aabdcd 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt @@ -1,13 +1,14 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth -import com.tangem.domain.models.account.CryptoPortfolioIcon.* +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color +import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon +import com.tangem.domain.models.wallet.UserWalletId import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject -import io.mockk.verify +import io.mockk.verifyOrder import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -23,159 +24,160 @@ class CryptoPortfolioIconTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class OfMainAccount { - @Test - fun `ofMainAccount with empty exclude`() { - // Act - val actual = CryptoPortfolioIcon.ofMainAccount(exclude = emptySet()) - - // Assert - val expectedColor = Color.Azure - Truth.assertThat(actual.color).isEqualTo(expectedColor) - - val expectedType = Type.Icon(value = Icon.Star) - Truth.assertThat(actual.type).isEqualTo(expectedType) - } - @ParameterizedTest @MethodSource("provideTestModels") fun ofMainAccount(model: OfMainAccountModel) { - // Arrange - mockkObject(Random.Default) - - val size = (Color.entries.size - model.exclude.size).takeIf { it > 0 } ?: Color.entries.size - every { Random.nextInt(size) } returns model.randomNextInt - // Act - val actual = CryptoPortfolioIcon.ofMainAccount(exclude = model.exclude) + val actual = CryptoPortfolioIcon.ofMainAccount(userWalletId = model.userWalletId) // Assert val expectedColor = model.expectedColor Truth.assertThat(actual.color).isEqualTo(expectedColor) - val expectedType = Type.Icon(value = Icon.Star) - Truth.assertThat(actual.type).isEqualTo(expectedType) - - verify(exactly = 1) { Random.nextInt(size) } - - unmockkObject(Random.Default) + val expectedIcon = Icon.Star + Truth.assertThat(actual.value).isEqualTo(expectedIcon) } private fun provideTestModels() = listOf( - // If the default color is already occupied (present in the exclude set), a random color from the - // remaining available colors will be selected for the main account icon. OfMainAccountModel( - exclude = setOf(Color.Azure), - randomNextInt = 0, - expectedColor = Color.entries[1], + userWalletId = UserWalletId("1234567890abcdef"), + expectedColor = Color.Pattypan, ), OfMainAccountModel( - exclude = setOf(Color.Azure, Color.CaribbeanBlue), - randomNextInt = 0, - expectedColor = Color.entries[2], + userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F"), + expectedColor = Color.CandyGrapeFizz, ), - // If all colors are already occupied, a random one will be selected. OfMainAccountModel( - exclude = Color.entries.toSet(), - randomNextInt = 1, - expectedColor = Color.entries[1], + userWalletId = UserWalletId("64A3791C180584C700EBECD6EAB36CBC34643BB449BC87761104C09F41DBCF3D"), + expectedColor = Color.PalatinateBlue, + ), + OfMainAccountModel( + userWalletId = UserWalletId("01C061A99FCCEDA87933267EBAB3513592F83AD2E27BDA6EE5546BA96009D21F"), + expectedColor = Color.Pelati, + ), + OfMainAccountModel( + userWalletId = UserWalletId("6D387A8FA5D2AF95F601EBCA8736D73D2ED53159835D8C407FBD4BBB10290C8B"), + expectedColor = Color.CaribbeanBlue, + ), + OfMainAccountModel( + userWalletId = UserWalletId("33FCD9B9982C31648C235AE55A29212D567ECD3BA24BE4227D1A01897ADBC959"), + expectedColor = Color.SweetDesire, + ), + OfMainAccountModel( + userWalletId = UserWalletId("197C8C5AA59270F3E9E1F30799A007D193DA596E6DC24C37D002C2EC203C2A0B"), + expectedColor = Color.VitalGreen, + ), + OfMainAccountModel( + userWalletId = UserWalletId("ACF90C18393828958B5E795771F0692A00D3D7ADC092F726AB4A7E3116DD6E6E"), + expectedColor = Color.Pattypan, ), ) } data class OfMainAccountModel( - val exclude: Set, - val randomNextInt: Int, + val userWalletId: UserWalletId, val expectedColor: Color, ) @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class OfCustomAccountBasedOnName { + inner class OfDefaultCustomAccount { @ParameterizedTest @MethodSource("provideTestModels") - fun ofCustomAccount(model: OfCustomAccountModel.BasedOnName) { + fun ofCustomAccount(model: OfDefaultCustomAccountModel) { // Arrange + val availableIcons = Icon.entries - setOf(Icon.Letter, Icon.Star) + mockkObject(Random.Default) - every { Random.nextInt(until = Color.entries.size) } returns model.randomNextInt + every { Random.nextInt(until = availableIcons.size) } returns model.randomIconIndex + every { Random.nextInt(until = Color.entries.size) } returns model.randomColorIndex // Act - val actual = CryptoPortfolioIcon.ofCustomAccount(accountName = model.accountName) + val actual = CryptoPortfolioIcon.ofDefaultCustomAccount() // Assert - val expectedColor = model.expectedColor - Truth.assertThat(actual.color).isEqualTo(expectedColor) + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) - val expectedType = Type.Symbol(value = model.accountName.first()) - Truth.assertThat(actual.type).isEqualTo(expectedType) - - verify(exactly = 1) { Random.nextInt(until = Color.entries.size) } + verifyOrder { + Random.nextInt(until = availableIcons.size) + Random.nextInt(until = Color.entries.size) + } unmockkObject(Random.Default) } private fun provideTestModels() = listOf( - OfCustomAccountModel.BasedOnName( - accountName = "New account", - randomNextInt = 0, - expectedColor = Color.entries[0], + OfDefaultCustomAccountModel( + randomIconIndex = 0, + randomColorIndex = 0, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.User, color = Color.Azure), ), - OfCustomAccountModel.BasedOnName( - accountName = "Awesome", - randomNextInt = Color.entries.lastIndex, - expectedColor = Color.entries.last(), + OfDefaultCustomAccountModel( + randomIconIndex = 1, + randomColorIndex = 1, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Family, color = Color.CaribbeanBlue), + ), + OfDefaultCustomAccountModel( + randomIconIndex = Icon.entries.lastIndex - 2, + randomColorIndex = Color.entries.lastIndex, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Gift, color = Color.VitalGreen), ), ) } + data class OfDefaultCustomAccountModel( + val randomIconIndex: Int, + val randomColorIndex: Int, + val expected: CryptoPortfolioIcon, + ) + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class OfCustomAccountWithTypeAndColor { @ParameterizedTest @MethodSource("provideTestModels") - fun ofCustomAccount(model: OfCustomAccountModel.WithTypeAndColor) { + fun ofCustomAccount(model: OfCustomAccountModel) { // Act - val actual = CryptoPortfolioIcon.ofCustomAccount(type = model.type, color = model.color) + val actual = CryptoPortfolioIcon.ofCustomAccount(value = model.icon, color = model.color) // Assert val expectedColor = model.expectedColor Truth.assertThat(actual.color).isEqualTo(expectedColor) val expectedType = model.expectedType - Truth.assertThat(actual.type).isEqualTo(expectedType) + Truth.assertThat(actual.value).isEqualTo(expectedType) } private fun provideTestModels() = listOf( - OfCustomAccountModel.WithTypeAndColor( - type = Type.Icon(value = Icon.User), + OfCustomAccountModel( + icon = Icon.User, color = Color.CaribbeanBlue, - expectedType = Type.Icon(value = Icon.User), + expectedType = Icon.User, expectedColor = Color.CaribbeanBlue, ), - OfCustomAccountModel.WithTypeAndColor( - type = Type.Symbol(value = 'A'), + OfCustomAccountModel( + icon = Icon.Letter, color = Color.DullLavender, - expectedType = Type.Symbol(value = 'A'), + expectedType = Icon.Letter, + expectedColor = Color.DullLavender, + ), + OfCustomAccountModel( + icon = Icon.Star, + color = Color.DullLavender, + expectedType = Icon.Star, expectedColor = Color.DullLavender, ), ) } - sealed interface OfCustomAccountModel { - - data class BasedOnName( - val accountName: String, - val randomNextInt: Int, - val expectedColor: Color, - ) : OfCustomAccountModel - - data class WithTypeAndColor( - val type: Type, - val color: Color, - val expectedType: Type, - val expectedColor: Color, - ) : OfCustomAccountModel - } + data class OfCustomAccountModel( + val icon: Icon, + val color: Color, + val expectedType: Icon, + val expectedColor: Color, + ) } \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt new file mode 100644 index 0000000000..f7daa8a9b2 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt @@ -0,0 +1,49 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DerivationIndexTest { + + @Test + fun `isMain returns true only for main derivation index`() { + // Arrange + val main = DerivationIndex.Main + val notMain = DerivationIndex(1).getOrNull()!! + + // Act & Assert + Truth.assertThat(main.isMain).isTrue() + Truth.assertThat(notMain.isMain).isFalse() + } + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: InvokeTestModel) { + // Act + val actual = DerivationIndex(model.index) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + InvokeTestModel(index = 0, expected = DerivationIndex.Main.right()), + InvokeTestModel(index = 5, expected = DerivationIndex(5).getOrNull()!!.right()), + InvokeTestModel(index = -1, expected = DerivationIndex.Error.NegativeDerivationIndex(-1).left()), + ) + + data class InvokeTestModel( + val index: Int, + val expected: Either, + ) +} \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt index f83aec887a..0fdd408cc5 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt @@ -1,8 +1,8 @@ package com.tangem.domain.nft.models -import com.tangem.domain.core.serialization.SerializedBigInteger import com.tangem.domain.models.StatusSource import com.tangem.domain.models.network.Network +import com.tangem.domain.models.serialization.SerializedBigInteger import kotlinx.serialization.Serializable @Serializable diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt index 43bc8a4a29..bac263726a 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt @@ -1,6 +1,6 @@ package com.tangem.domain.nft.models -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable @Serializable diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt index 963d3b99d7..289328d75d 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -2,25 +2,25 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class GetApplicationIdUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, ) { private val mutex = Mutex() suspend operator fun invoke(): Either = Either.catch { - val localApplicationId = notificationsRepository.getApplicationId() + val localApplicationId = pushNotificationsRepository.getApplicationId() if (localApplicationId != null) return@catch localApplicationId mutex.withLock { - val doubleCheckedId = notificationsRepository.getApplicationId() + val doubleCheckedId = pushNotificationsRepository.getApplicationId() if (doubleCheckedId != null) return@withLock doubleCheckedId - val newApplicationId = notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(newApplicationId) + val newApplicationId = pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(newApplicationId) newApplicationId } } diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt index edc3de5294..9264da1a16 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt @@ -2,13 +2,13 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.NotificationsEligibleNetwork -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository class GetNetworksAvailableForNotificationsUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, ) { suspend operator fun invoke(): Either> = Either.catch { - notificationsRepository.getEligibleNetworks() + pushNotificationsRepository.getEligibleNetworks() } } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt index 63247b9a7d..d33888f342 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt @@ -2,16 +2,16 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider class SendPushTokenUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, ) { suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val token = pushNotificationsTokenProvider.getToken() - notificationsRepository.sendPushToken(applicationId, token) + pushNotificationsRepository.sendPushToken(applicationId, token) } } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt new file mode 100644 index 0000000000..44413fe106 --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.NotificationsRepository + +class SetShouldShowNotificationUseCase( + private val notificationsRepository: NotificationsRepository, +) { + + suspend operator fun invoke(key: String, value: Boolean) { + notificationsRepository.setShouldShowNotifications(key, value) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt new file mode 100644 index 0000000000..657ab7819d --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.NotificationsRepository + +class ShouldShowNotificationUseCase( + private val notificationsRepository: NotificationsRepository, +) { + + suspend operator fun invoke(key: String): Boolean { + return notificationsRepository.shouldShowNotification(key) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index 59f5e6e58b..0f1d3cb606 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,27 +1,43 @@ package com.tangem.domain.notifications.repository -import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.models.NotificationsEligibleNetwork - +/** + * Repository interface for managing local notification logic and state. + * + * This interface provides methods to check and update whether specific notifications should be shown, + * as well as to track the display count for certain notifications (e.g., Tron token fee). + * + * Note: This repository is responsible only for the local logic and state (such as preferences and counters) + * regarding notifications. It does **not** directly show or hide notifications to the user. + * The actual display and hiding of notifications in the UI is handled by [NotificationsUM], + * which uses this repository to determine the appropriate behavior. + */ interface NotificationsRepository { - @Throws - suspend fun createApplicationId(pushToken: String? = null): ApplicationId + /** + * Checks whether a notification with the given [key] should be shown to the user. + * @param key The unique identifier for the notification. + * @return true if the notification should be shown, false otherwise. + */ + suspend fun shouldShowNotification(key: String): Boolean - suspend fun saveApplicationId(appId: ApplicationId) - - suspend fun getApplicationId(): ApplicationId? + /** + * Sets whether a notification with the given [key] should be shown to the user. + * @param key The unique identifier for the notification. + * @param value true if the notification should be shown, false otherwise. + */ + suspend fun setShouldShowNotifications(key: String, value: Boolean) + /** + * Gets the number of times the Tron token fee notification has been shown. + * @return The current show counter for the Tron token fee notification. + */ suspend fun getTronTokenFeeNotificationShowCounter(): Int + /** + * Increments the counter tracking how many times the Tron token fee notification has been shown. + */ suspend fun incrementTronTokenFeeNotificationShowCounter() - @Throws - suspend fun sendPushToken(appId: ApplicationId, pushToken: String) - - @Throws - suspend fun getEligibleNetworks(): List - suspend fun shouldShowSubscribeOnNotificationsAfterUpdate(): Boolean suspend fun isUserAllowToSubscribeOnPushNotifications(): Boolean diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt new file mode 100644 index 0000000000..338a74b0ce --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.notifications.repository + +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork + +interface PushNotificationsRepository { + + @Throws + suspend fun createApplicationId(pushToken: String? = null): ApplicationId + + suspend fun saveApplicationId(appId: ApplicationId) + + suspend fun getApplicationId(): ApplicationId? + + @Throws + suspend fun sendPushToken(appId: ApplicationId, pushToken: String) + + @Throws + suspend fun getEligibleNetworks(): List +} \ No newline at end of file diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt index 6b2984554b..e3a191f205 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -3,7 +3,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -15,14 +15,14 @@ import java.net.SocketTimeoutException class GetApplicationIdUseCaseTest { - private val notificationsRepository: NotificationsRepository = mockk() - private val useCase = GetApplicationIdUseCase(notificationsRepository) + private val pushNotificationsRepository: PushNotificationsRepository = mockk() + private val useCase = GetApplicationIdUseCase(pushNotificationsRepository) @Test fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest { // GIVEN val expectedApplicationId = ApplicationId("test-app-id") - coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId + coEvery { pushNotificationsRepository.getApplicationId() } returns expectedApplicationId // WHEN val result = useCase() @@ -30,10 +30,10 @@ class GetApplicationIdUseCaseTest { // THEN assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(expectedApplicationId) - coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() } coVerify(inverse = true) { - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(any()) + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(any()) } } @@ -41,9 +41,9 @@ class GetApplicationIdUseCaseTest { fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest { // GIVEN val newApplicationId = ApplicationId("new-app-id") - coEvery { notificationsRepository.getApplicationId() } returns null - coEvery { notificationsRepository.createApplicationId() } returns newApplicationId - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.getApplicationId() } returns null + coEvery { pushNotificationsRepository.createApplicationId() } returns newApplicationId + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val result = useCase() @@ -52,10 +52,10 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) coVerifyOrder { - notificationsRepository.getApplicationId() - notificationsRepository.getApplicationId() - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(newApplicationId) + pushNotificationsRepository.getApplicationId() + pushNotificationsRepository.getApplicationId() + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(newApplicationId) } } @@ -63,7 +63,7 @@ class GetApplicationIdUseCaseTest { fun `GIVEN repository throws exception WHEN invoke THEN return Either Left with error`() = runTest { // GIVEN val expectedError = SocketTimeoutException("Test error") - coEvery { notificationsRepository.getApplicationId() } throws expectedError + coEvery { pushNotificationsRepository.getApplicationId() } throws expectedError // WHEN val result = useCase() @@ -71,10 +71,10 @@ class GetApplicationIdUseCaseTest { // THEN assertThat(result).isInstanceOf(Either.Left::class.java) assertThat((result as Either.Left).value).isEqualTo(expectedError) - coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() } coVerify(inverse = true) { - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(any()) + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(any()) } } @@ -85,14 +85,14 @@ class GetApplicationIdUseCaseTest { val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false - coEvery { notificationsRepository.getApplicationId() } answers { + coEvery { pushNotificationsRepository.getApplicationId() } answers { if (!isIdCreated) null else newApplicationId } - coEvery { notificationsRepository.createApplicationId() } answers { + coEvery { pushNotificationsRepository.createApplicationId() } answers { isIdCreated = true newApplicationId } - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val results = coroutineScope { @@ -108,9 +108,9 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) } - coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } - coVerify(exactly = 1) { notificationsRepository.createApplicationId() } - coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) } } @Test @@ -119,14 +119,14 @@ class GetApplicationIdUseCaseTest { val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false - coEvery { notificationsRepository.getApplicationId() } answers { + coEvery { pushNotificationsRepository.getApplicationId() } answers { if (!isIdCreated) null else newApplicationId } - coEvery { notificationsRepository.createApplicationId() } answers { + coEvery { pushNotificationsRepository.createApplicationId() } answers { isIdCreated = true newApplicationId } - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val results = coroutineScope { @@ -143,9 +143,9 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) } - coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } - coVerify(exactly = 1) { notificationsRepository.createApplicationId() } - coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) } } companion object { diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt index 18d716fdf0..fe84d79f16 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -3,7 +3,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider import io.mockk.coEvery import io.mockk.coVerify @@ -14,16 +14,16 @@ import org.junit.Test class SendPushTokenUseCaseTest { - private lateinit var notificationsRepository: NotificationsRepository + private lateinit var pushNotificationsRepository: PushNotificationsRepository private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider private lateinit var sendPushTokenUseCase: SendPushTokenUseCase @Before fun setup() { - notificationsRepository = mockk() + pushNotificationsRepository = mockk() pushNotificationsTokenProvider = mockk() sendPushTokenUseCase = SendPushTokenUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -34,14 +34,14 @@ class SendPushTokenUseCaseTest { val applicationId = ApplicationId("test-app-id") val token = "test-token" coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Unit // WHEN val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Right(Unit)) - coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } @Test @@ -51,13 +51,13 @@ class SendPushTokenUseCaseTest { val token = "test-token" val expectedError = RuntimeException("Network error") coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } throws expectedError // WHEN val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Left(expectedError)) - coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt index a6ffb9df16..29e09d2151 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt @@ -1,6 +1,6 @@ package com.tangem.domain.onramp.model -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable @Serializable diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt index fb76a29bea..352c2d75e2 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -2,10 +2,10 @@ package com.tangem.domain.onramp.model.cache import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampStatus -import com.tangem.domain.models.wallet.UserWalletId /** * Model for local storing onramp transaction diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt index ab5a2807a0..1a2b6103e1 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt @@ -2,9 +2,9 @@ package com.tangem.domain.onramp import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository class GetLegacyTopUpUrlUseCase( private val legacyTopUpRepository: LegacyTopUpRepository, diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 1a64989894..dd8608db95 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -24,8 +24,10 @@ interface SettingsRepository { suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun shouldSaveAccessCodes(): Boolean + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun setShouldSaveAccessCodes(value: Boolean) suspend fun incrementAppLaunchCounter() diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 770bfe4838..7b65991ac1 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.core.analytics) api(projects.core.utils) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts index 10abc551b9..5d2fa9274e 100644 --- a/domain/staking/models/build.gradle.kts +++ b/domain/staking/models/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.models) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt deleted file mode 100644 index 6313b9277c..0000000000 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.tangem.domain.staking.model - -// TODO: make part of YieldBalance in the future -data class StakingID(val integrationId: String, val address: String) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index 063bcce89a..093fd13bef 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -1,13 +1,14 @@ package com.tangem.domain.staking.model.stakekit -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import com.tangem.domain.models.staking.YieldToken import kotlinx.serialization.Serializable @Serializable data class Yield( val id: String, - val token: Token, - val tokens: List, + val token: YieldToken, + val tokens: List, val args: Args, val status: Status, val apy: SerializedBigDecimal, @@ -93,9 +94,9 @@ data class Yield( val logoUri: String, val description: String, val documentation: String?, - val gasFeeToken: Token, - val token: Token, - val tokens: List, + val gasFeeToken: YieldToken, + val token: YieldToken, + val tokens: List, val type: String, val rewardSchedule: RewardSchedule, val cooldownPeriod: Period?, @@ -145,18 +146,6 @@ data class Yield( } } -@Serializable -data class Token( - val name: String, - val network: NetworkType, - val symbol: String, - val decimals: Int, - val address: String?, - val coinGeckoId: String?, - val logoURI: String?, - val isPoints: Boolean?, -) - @Serializable data class AddressArgument( val required: Boolean, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt index be1545d6da..bfe0503203 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt @@ -1,5 +1,6 @@ package com.tangem.domain.staking.model.stakekit.action +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import org.joda.time.DateTime import java.math.BigDecimal diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt index 605ed69fa5..1bf184eded 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt @@ -1,8 +1,8 @@ package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.models.staking.YieldToken +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import java.math.BigDecimal data class ActionParams( @@ -11,7 +11,7 @@ data class ActionParams( val amount: BigDecimal, val address: String, val validatorAddress: String, - val token: Token, + val token: YieldToken, val publicKey: String? = null, val passthrough: String? = null, val type: StakingActionType? = null, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt index 585ae1edb5..7f8562f492 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt @@ -1,10 +1,10 @@ package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.models.staking.YieldToken import java.math.BigDecimal data class StakingGasEstimate( val amount: BigDecimal, - val token: Token, + val token: YieldToken, val gasLimit: String?, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt index 4222203a21..058a7d1e6d 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt @@ -1,6 +1,6 @@ package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType data class StakingTransaction( val id: String, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt index bf3fb63499..f44adef4ae 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.repositories.StakingActionRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 24b94b0ff2..0308e969e5 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -1,36 +1,41 @@ package com.tangem.domain.staking import arrow.core.Either -import arrow.core.raise.catch +import arrow.core.getOrElse import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher class FetchStakingYieldBalanceUseCase( - private val stakingErrorResolver: StakingErrorResolver, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return either { - catch( - block = { - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), - ) - }, - catch = { stakingErrorResolver.resolve(it) }, - ) - } + ): Either = either { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ) + .getOrElse { + when (it) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) + StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() + } + + return@either + } + + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + ) + .mapLeft { StakingError.DomainError("$it") } + .bind() } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt index 34d0c03b27..c3c236025c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt @@ -1,16 +1,18 @@ package com.tangem.domain.staking -import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.models.staking.action.StakingActionType import java.math.BigDecimal -class GetActionRequirementAmountUseCase( - private val stakingRepository: StakingRepository, -) { +class GetActionRequirementAmountUseCase { - operator fun invoke(integrationId: String, actionType: StakingActionType): Either = - Either.catch { - stakingRepository.getActionRequirementAmount(integrationId, actionType) + operator fun invoke(integrationId: String, actionType: StakingActionType): BigDecimal? { + return if (StakingIntegrationID.EthereumToken.Polygon.value == integrationId && + actionType == StakingActionType.CLAIM_REWARDS + ) { + BigDecimal.ONE + } else { + null } + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt deleted file mode 100644 index 2c9d213e72..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.staking - -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.repositories.StakingRepository - -class GetStakingIntegrationIdUseCase( - private val stakingRepository: StakingRepository, -) { - - operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID) = - stakingRepository.getSupportedIntegrationId(cryptoCurrencyId) -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 2b1a377476..f24cac1f74 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -1,10 +1,14 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.YieldToken +import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.utils.extensions.isEqualTo import java.math.BigDecimal @@ -17,7 +21,7 @@ class InvalidatePendingTransactionsUseCase( operator fun invoke( balanceItems: List, stakingActions: List, - token: Token, + token: YieldToken, ): Either> { return Either.catch { val balancesToDisplay = mergeBalancesAndProcessingActions( @@ -34,7 +38,7 @@ class InvalidatePendingTransactionsUseCase( private fun mergeBalancesAndProcessingActions( realBalances: List, processingActions: List, - token: Token, + token: YieldToken, ): List { val balances = realBalances.toMutableList() @@ -87,7 +91,7 @@ class InvalidatePendingTransactionsUseCase( private fun addStubStakedPendingTransaction( balances: MutableList, action: StakingAction, - token: Token, + token: YieldToken, ) { balances.add( BalanceItem( @@ -153,7 +157,7 @@ class InvalidatePendingTransactionsUseCase( return index to action.amount } - private fun doPostProcessing(balances: MutableList, action: StakingAction, token: Token) { + private fun doPostProcessing(balances: MutableList, action: StakingAction, token: YieldToken) { val validatorAddress = action.validatorAddress ?: action.validatorAddresses?.firstOrNull() if (token.network == NetworkType.TON && validatorAddress != null) { for (index in balances.indices) { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt deleted file mode 100644 index 549dbb614c..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingRepository - -class IsApproveNeededUseCase( - private val stakingRepository: StakingRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - operator fun invoke(cryptoCurrency: CryptoCurrency): Either { - return Either - .catch { stakingRepository.getStakingApproval(cryptoCurrency) } - .mapLeft { stakingErrorResolver.resolve(it) } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt index 0da69ebb56..c8648ad64c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt @@ -6,7 +6,7 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.walletmanager.WalletManagersFacade @@ -42,12 +42,37 @@ class StakingIdFactory( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network, + ): Either { + return createInternal( + currencyId = currencyId, + defaultAddressProvider = { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + }, + ) + } + + /** + * Creates a [StakingID] for the given cryptocurrency and default address + * + * @param currencyId the identifier of the cryptocurrency + * @param defaultAddress the default address for staking, can be null + */ + fun create(currencyId: CryptoCurrency.ID, defaultAddress: String?): Either { + return createInternal( + currencyId = currencyId, + defaultAddressProvider = { defaultAddress }, + ) + } + + private inline fun createInternal( + currencyId: CryptoCurrency.ID, + defaultAddressProvider: () -> String?, ): Either = either { val integrationId = StakingIntegrationID.create(currencyId = currencyId) ensureNotNull(integrationId) { Error.UnsupportedCurrency } - val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() } ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index 063e0e9f70..f8376e3707 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -4,7 +4,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.staking.analytics.StakingAnalyticsEvent.ButtonRewards.addIfValueIsNotNull import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType sealed class StakingAnalyticsEvent( event: String, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt index 397dd9f7de..041755dbc8 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt @@ -1,9 +1,8 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowFetcher -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.staking.StakingID /** * Fetcher of yields balances @@ -15,23 +14,19 @@ interface MultiYieldBalanceFetcher : FlowFetcher, + val stakingIds: Set, ) { override fun toString(): String { - val currencyIdWithNetworkMap = currencyIdWithNetworkMap.entries.joinToString { - "${it.key.value} - ${it.value}" - } - return """ MultiYieldBalanceFetcher.Params( userWalletId = $userWalletId, - currencyIdWithNetworkMap: $currencyIdWithNetworkMap + stakingIds: ${stakingIds.joinToString()} ) """.trimIndent() } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt index b5b2446198..7b9357cb24 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt index 4c6cbed1f3..6d6f106113 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance /** * Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params] diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index dce26456cb..efbbae2c38 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -6,26 +6,20 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import kotlinx.coroutines.flow.Flow -import java.math.BigDecimal @Suppress("TooManyFunctions") interface StakingRepository { - fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? - suspend fun fetchEnabledYields() suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo @@ -48,13 +42,6 @@ interface StakingRepository { stakingActionStatus: StakingActionStatus, ): List - suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance - - suspend fun getMultiYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): List? - suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate @@ -66,13 +53,5 @@ interface StakingRepository { transactionId: String, ): Pair - /** Returns staking approval */ - fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval - suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean - - /** - * Return action requirement amount - */ - fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt index ffff57fcff..61a7ee90c6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt @@ -1,9 +1,8 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowFetcher -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.staking.StakingID /** * Fetcher of yield balance @@ -16,12 +15,10 @@ interface SingleYieldBalanceFetcher : FlowFetcher { data class Params( val userWalletId: UserWalletId, - val currencyId: CryptoCurrency.ID, - val network: Network, + val stakingId: StakingID, ) { override fun toString(): String { return """ SingleYieldBalanceProducer.Params( userWalletId = $userWalletId, - currencyId = $currencyId, - network = $network + stakingId = $stakingId, ) """.trimIndent() } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt index f7509dc980..4d93e733a1 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance /** * Supplier of yield balance for selected wallet [SingleYieldBalanceProducer.Params] diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt index fc7a759b50..f6b6a96d42 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking.utils -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.lib.crypto.BlockchainUtils import java.math.BigDecimal diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index cd77c25e9e..09f2670173 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -9,7 +9,7 @@ import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.ProvideTestModels import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.walletmanager.WalletManagersFacade import io.mockk.clearMocks diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index acfaaf4d46..81eb91fe42 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -1,7 +1,7 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus /** * Model of currencies available to swap diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt index 5e8e0479f9..863d43017e 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt @@ -1,7 +1,7 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus /** * Domain layer representation of SwapPair data network model. diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt index e296ca6fe3..7bd7a99414 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt @@ -26,4 +26,5 @@ data class SwapTransactionModel( val toCryptoAmount: BigDecimal, val provider: ExpressProvider, val status: SwapStatusModel? = null, + val swapTxType: SwapTxType? = SwapTxType.Swap, ) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTxType.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTxType.kt new file mode 100644 index 0000000000..0ad4a8ee0c --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTxType.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.swap.models + +enum class SwapTxType { + Swap, + SendWithSwap, +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index d3a0b037c7..b94e923f2f 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -4,12 +4,9 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapDataModel -import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.swap.models.SwapQuoteModel -import com.tangem.domain.swap.models.SwapStatusModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.swap.models.* import java.math.BigDecimal /** @@ -25,12 +22,14 @@ interface SwapRepositoryV2 { * @param initialCurrency currency being swapped (either to or from) * @param cryptoCurrencyStatusList list of currencies might be swapped * @param filterProviderTypes filters only specified provider types, if empty returns providers as is + * @param swapTxType swap tx type */ suspend fun getPairs( userWallet: UserWallet, initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ): List /** @@ -43,6 +42,7 @@ interface SwapRepositoryV2 { initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ): List /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt index 2f34f53067..0ad2638066 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt @@ -1,11 +1,11 @@ package com.tangem.domain.swap import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.swap.models.SwapStatusModel import com.tangem.domain.swap.models.SwapTransactionListModel import com.tangem.domain.swap.models.SwapTransactionModel -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow /** @@ -34,7 +34,7 @@ interface SwapTransactionRepository { * @param userWallet selected user wallet * @param cryptoCurrencyId transactions for specific crypto currency */ - suspend fun getTransactions( + fun getTransactions( userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, ): Flow?> diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt index 6370564cc2..ac60b660e2 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -5,11 +5,11 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.SwapDataModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus @Suppress("LongParameterList") class GetSwapDataUseCase( diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt index 55fe6a22b4..1284c416c3 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt @@ -3,14 +3,15 @@ package com.tangem.domain.swap.usecase import arrow.core.Either import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.SwapCryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.swap.models.SwapTxType /** * Get list of swap pairs @@ -31,12 +32,14 @@ class GetSwapPairsUseCase( initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ) = Either.catch { val pairs = swapRepositoryV2.getPairs( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyStatusList = cryptoCurrencyStatusList, filterProviderTypes = filterProviderTypes, + swapTxType = swapTxType, ) val fromGroup = pairs.groupPairs( diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index db527b65a1..6ef54ca2bd 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.swap.usecase import arrow.core.Either import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 @@ -10,7 +11,7 @@ import com.tangem.domain.swap.models.SwapCryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.swap.models.SwapTxType /** * Returns pais @@ -25,12 +26,14 @@ class GetSwapSupportedPairsUseCase( initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ) = Either.catch { val pairs = swapRepositoryV2.getSupportedPairs( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyList = cryptoCurrencyList, filterProviderTypes = filterProviderTypes, + swapTxType = swapTxType, ) val filteredOutInitial = cryptoCurrencyList.filterNot { it.id == initialCurrency.id } @@ -64,7 +67,9 @@ class GetSwapSupportedPairsUseCase( // Search available to swap currency .filter { pair -> cryptoCurrencyList.any { currencyStatus -> - currencyStatus.id == pair.to.currency.id + // Allowed only on networks without tx extras (e.i. memo and destination tag) + val isExtrasSupported = currencyStatus.network.transactionExtrasType.isTxExtrasSupported() + currencyStatus.id == pair.to.currency.id && !isExtrasSupported } }.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt index ed6c8a1d27..dcfd5cffb4 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt @@ -1,13 +1,13 @@ package com.tangem.domain.swap.usecase import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapTransactionRepository import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.getGroupWithDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.extensions.orZero /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index e254c348c4..8d6869366a 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -3,15 +3,12 @@ package com.tangem.domain.swap.usecase import arrow.core.Either import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType.Companion.shouldStoreSwapTransaction +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.SwapTransactionRepository -import com.tangem.domain.swap.models.SwapDataTransactionModel -import com.tangem.domain.swap.models.SwapStatus -import com.tangem.domain.swap.models.SwapStatusModel -import com.tangem.domain.swap.models.SwapTransactionModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.swap.models.* @Suppress("LongParameterList") class SwapTransactionSentUseCase( @@ -28,15 +25,8 @@ class SwapTransactionSentUseCase( provider: ExpressProvider, txHash: String, timestamp: Long, + swapTxType: SwapTxType, ) = Either.catch { - swapRepositoryV2.swapTransactionSent( - userWallet = userWallet, - fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, - toAddress = swapDataTransactionModel.txTo, - txId = swapDataTransactionModel.txId, - txHash = txHash, - txExtraId = swapDataTransactionModel.txExtraId, - ) if (provider.type.shouldStoreSwapTransaction()) { swapTransactionRepository.storeTransaction( userWalletId = userWallet.walletId, @@ -56,12 +46,22 @@ class SwapTransactionSentUseCase( txExternalId = (swapDataTransactionModel as? SwapDataTransactionModel.CEX)?.externalTxId, averageDuration = null, ), + swapTxType = swapTxType, ), ) } + swapTransactionRepository.storeLastSwappedCryptoCurrencyId( userWalletId = userWallet.walletId, cryptoCurrencyId = toCryptoCurrencyStatus.currency.id, ) + swapRepositoryV2.swapTransactionSent( + userWallet = userWallet, + fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, + toAddress = swapDataTransactionModel.txTo, + txId = swapDataTransactionModel.txId, + txHash = txHash, + txExtraId = swapDataTransactionModel.txExtraId, + ) }.mapLeft(swapErrorResolver::resolve) } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 2bcbd75a88..a1d0b5b08f 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -54,6 +54,10 @@ dependencies { implementation(deps.jodatime) implementation(deps.reKotlin) + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + /** Tests */ testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) @@ -61,7 +65,4 @@ dependencies { testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) - testImplementation(tangemDeps.blockchain) { - exclude(module = "joda-time") - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 936727eead..06b498b2ab 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -1,14 +1,17 @@ package com.tangem.domain.tokens import arrow.core.Either +import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.async @@ -29,6 +32,7 @@ class AddCryptoCurrenciesUseCase( private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -154,14 +158,28 @@ class AddCryptoCurrenciesUseCase( currenciesRepository.syncTokens(userWalletId) } - private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) { - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = addedCurrency.id, - network = addedCurrency.network, - ), + private suspend fun refreshUpdatedYieldBalances( + userWalletId: UserWalletId, + addedCurrency: CryptoCurrency, + ): Either = either { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = addedCurrency.id, + network = addedCurrency.network, ) + .getOrElse { + when (it) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) + StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() + } + + return@either + } + + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + ) + .bind() } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 82b0862695..2c491ffdee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -6,12 +6,13 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -21,6 +22,7 @@ class FetchCardTokenListUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { @@ -83,11 +85,12 @@ class FetchCardTokenListUseCase( } private suspend fun fetchYieldBalances(userWalletId: UserWalletId, currencies: List) { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 6cb6d54c1d..e500c445fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -1,18 +1,20 @@ package com.tangem.domain.tokens import arrow.core.Either +import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -32,6 +34,7 @@ class FetchCurrencyStatusUseCase( private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -136,14 +139,25 @@ class FetchCurrencyStatusUseCase( private suspend fun fetchStakingBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), + ): Either = either { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, ) + .getOrElse { + when (it) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) + StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() + } + + return@either + } + + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + ) + .bind() } private fun List>.summarizeResult(): Either { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 6206a7203b..d4df6acfcd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -8,12 +8,13 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -29,6 +30,7 @@ class FetchTokenListUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { /** @@ -104,11 +106,12 @@ class FetchTokenListUseCase( } private suspend fun fetchYieldBalances(userWalletId: UserWalletId, currencies: List) { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt index 0d6b285977..9bba447d40 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -2,13 +2,13 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt index 807e118321..bc8e4fa72d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt @@ -2,11 +2,11 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e1b0f65499..e219fbf94c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -3,6 +3,9 @@ package com.tangem.domain.tokens import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.promo.models.StoryContentIds @@ -12,11 +15,8 @@ import com.tangem.domain.tokens.actions.CommonActionsFactory import com.tangem.domain.tokens.actions.MissedDerivationsActionsFactory import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory import com.tangem.domain.tokens.actions.UnreachableActionsFactory -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt index 329794c3ae..43c65d397a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -1,10 +1,10 @@ package com.tangem.domain.tokens import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrencyChecksRepository -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index eb1f7d4bd6..6b64410367 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -3,8 +3,9 @@ package com.tangem.domain.tokens import com.tangem.blockchainsdk.utils.isNeedToCreateAccountWithoutReserve import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -15,7 +16,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt index 640a3f0726..77bb61fb5e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt @@ -2,12 +2,12 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId class GetFeePaidCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt index 72ee9c9df4..71f50c03d7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt @@ -2,9 +2,9 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.repository.CurrencyChecksRepository import java.math.BigDecimal class GetMinimumTransactionAmountSyncUseCase( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt index 34faf60620..63b74c9631 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt @@ -1,12 +1,12 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow class GetMultiCryptoCurrencyStatusUseCase( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 5179b54d12..89a9baade6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -2,15 +2,15 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt index 1bd4c4f678..369303c3cb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt @@ -2,12 +2,12 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index fde614c26c..042f4d257f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -4,14 +4,14 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.transformLatest class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { @OptIn(ExperimentalCoroutinesApi::class) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index c3a08ac9a3..0fc94390be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -9,10 +9,10 @@ import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -20,7 +20,7 @@ import timber.log.Timber import java.util.concurrent.ConcurrentHashMap class GetWalletTotalBalanceUseCase( - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { private val walletBalanceCache = ConcurrentHashMap() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index 936723bc4d..b0b9ac0e7c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -6,9 +6,9 @@ import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.withError import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt index e282aed62d..9283f50615 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -7,9 +7,9 @@ import arrow.core.raise.ensure import arrow.core.raise.withError import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index f8285e582d..2809346d58 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -2,11 +2,11 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index 47e33ba88b..180041ff13 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -2,10 +2,10 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index 288ef4398d..2e0d1cbf54 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -2,9 +2,9 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt index cd59aafb14..e2c0e5ff7a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -1,8 +1,8 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt index 5332319220..e1afb549a0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.error -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList sealed class TokenListError { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index b7f00f81cb..1cf1651c9d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.legacy -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import org.rekotlin.Action sealed class TradeCryptoAction : Action { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt deleted file mode 100644 index 19aae81b6d..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.models.network.Network - -/** - * Represents a group of cryptocurrencies associated with a specific network. - * - * This class encapsulates a collection of cryptocurrency statuses, all of which are part of the same blockchain network. - * - * @property network The blockchain network associated with the group. - * @property currencies A list of cryptocurrency statuses that belong to the network. - */ -data class NetworkGroup( - val network: Network, - val currencies: List, -) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index d822bddd38..306a8cba00 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -1,7 +1,8 @@ package com.tangem.domain.tokens.model -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.stakekit.Yield data class TokenActionsState( val walletId: UserWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt deleted file mode 100644 index 1ade11835e..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import java.math.BigDecimal - -/** - * Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped. - * - * The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection. - * Additional details like the total fiat balance and the sorting type can be associated with the list. - * - * @property totalFiatBalance The total fiat balance across all tokens, which could be in a loading state, failed, or loaded with a specific amount. - * @property sortedBy The criteria used for sorting the tokens. - */ -sealed class TokenList { - open val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loading - open val sortedBy: TokensSortType = TokensSortType.NONE - - /** - * Represents tokens that are grouped by their network. - * - * @property groups A list of network groups containing tokens. - * @property totalFiatBalance The total fiat balance across all groups. - * @property sortedBy The criteria used for sorting the tokens within the groups. - */ - data class GroupedByNetwork( - val groups: List, - override val totalFiatBalance: TotalFiatBalance, - override val sortedBy: TokensSortType, - ) : TokenList() - - /** - * Represents tokens that are not grouped by any specific criteria. - * - * @property currencies A list of cryptocurrency statuses. - * @property totalFiatBalance The total fiat balance across all currencies. - * @property sortedBy The criteria used for sorting the currencies. - */ - data class Ungrouped( - val currencies: List, - override val totalFiatBalance: TotalFiatBalance, - override val sortedBy: TokensSortType, - ) : TokenList() - - /** Represents a state where the token list is empty. */ - data object Empty : TokenList() { - - override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( - amount = BigDecimal.ZERO, - isAllAmountsSummarized = true, - source = StatusSource.ACTUAL, - ) - } - - /** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */ - fun flattenCurrencies(): List { - return when (this) { - is GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies) - is Ungrouped -> currencies - is Empty -> emptyList() - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt index 11ad1a755f..d3842665be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt @@ -1,9 +1,9 @@ package com.tangem.domain.tokens.operations import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus /** * Base operations for working with currencies statuses diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 6e242d03c6..3a258c4cf6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -1,15 +1,17 @@ package com.tangem.domain.tokens.operations import arrow.core.* -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.raise.recover +import arrow.core.raise.* +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -18,14 +20,16 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.staking.model.isStakingSupported +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -35,7 +39,6 @@ import kotlinx.coroutines.flow.* * Base operations for working with currency status * * @property currenciesRepository repository for currencies - * @property stakingRepository repository for staking * [REDACTED_AUTHOR] */ @@ -43,16 +46,19 @@ import kotlinx.coroutines.flow.* abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, ) { - protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository) + protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator() + + abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> @@ -82,7 +88,7 @@ abstract class BaseCurrencyStatusOperations( return getCurrencyStatusFlow(userWalletId = userWalletId, currency = currency) } - fun getCurrencyStatusFlow( + suspend fun getCurrencyStatusFlow( userWalletId: UserWalletId, currency: CryptoCurrency, includeQuotes: Boolean = true, @@ -105,9 +111,24 @@ abstract class BaseCurrencyStatusOperations( val statusFlow = getNetworkStatus(userWalletId = userWalletId, network = currency.network) - val yieldBalanceFlow = getYieldBalance(userWalletId = userWalletId, cryptoCurrency = currency) + val isStakingSupported = currency.network.toBlockchain().isStakingSupported - return if (subscribeOnYieldBalance) { + val yieldBalanceFlow = if (isStakingSupported) { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = currency.id, + network = currency.network, + ) + .getOrNull() + + stakingId?.let { + getYieldBalance(userWalletId = userWalletId, stakingId = it) + } + } else { + null + } + + return if (subscribeOnYieldBalance && yieldBalanceFlow != null) { combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, @@ -334,15 +355,11 @@ abstract class BaseCurrencyStatusOperations( .bind() } - private fun getYieldBalance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): EitherFlow { + private fun getYieldBalance(userWalletId: UserWalletId, stakingId: StakingID): EitherFlow { return singleYieldBalanceSupplier( params = SingleYieldBalanceProducer.Params( userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, + stakingId = stakingId, ), ) .map> { it.right() } @@ -396,34 +413,44 @@ abstract class BaseCurrencyStatusOperations( private suspend fun getYieldBalancesSync( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Either> { - return catch( - block = { - val balances = stakingRepository.getMultiYieldBalanceSync( - userWalletId = userWalletId, - cryptoCurrencies = cryptoCurrencies, - ) + ): Either> = either { + val stakingIds = cryptoCurrencies.mapNotNull { cryptoCurrency -> + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) + .getOrNull() + } - if (balances.isNullOrEmpty()) { - Error.EmptyYieldBalances.left() - } else { - balances.right() - } - }, - catch = { Error.EmptyYieldBalances.left() }, + ensure(stakingIds.isNotEmpty()) { Error.EmptyYieldBalances } + + val balances = multiYieldBalanceSupplier.getSyncOrNull( + params = MultiYieldBalanceProducer.Params(userWalletId = userWalletId), ) + .orEmpty() + .filter { it.stakingId in stakingIds } + + ensure(balances.isNotEmpty()) { Error.EmptyYieldBalances } + + balances } private suspend fun getYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return catch( - block = { stakingRepository.getSingleYieldBalanceSync(userWalletId, cryptoCurrency).right() }, - catch = { - Error.EmptyYieldBalances.left() - }, + ): Either = either { + val stakingId = stakingIdFactory.create(userWalletId, cryptoCurrency) + .mapLeft { + val exception = IllegalStateException("$it") + Error.DataError(exception) + } + .bind() + + val yieldBalance = singleYieldBalanceSupplier.getSyncOrNull( + params = SingleYieldBalanceProducer.Params( + userWalletId = userWalletId, + stakingId = stakingId, + ), ) + + ensureNotNull(yieldBalance) { Error.EmptyYieldBalances } } private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 2861f1eb19..c82e831875 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -11,9 +11,12 @@ import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -24,16 +27,15 @@ import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalance.Unsupported.integrationId +import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.extractAddress @@ -46,7 +48,6 @@ import kotlinx.coroutines.flow.* class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - private val stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -54,21 +55,23 @@ class CachedCurrenciesStatusesOperations( private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceSupplier: MultiYieldBalanceSupplier, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, -) : BaseCurrenciesStatusesOperations, - BaseCurrencyStatusOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - stakingRepository = stakingRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - tokensFeatureToggles = tokensFeatureToggles, - ) { +) : BaseCurrencyStatusOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + stakingIdFactory = stakingIdFactory, + tokensFeatureToggles = tokensFeatureToggles, +) { override fun getCurrenciesStatuses( userWalletId: UserWalletId, @@ -79,6 +82,7 @@ class CachedCurrenciesStatusesOperations( ) } + @Suppress("LongMethod") @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, @@ -159,10 +163,23 @@ class CachedCurrenciesStatusesOperations( .invokeOnCompletion { setFetchFinished(userWalletId) } } + val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) + combine( flow = getQuotes(currenciesIds), - flow2 = getNetworkStatusesUpdates(userWalletId, networks), - flow3 = getYieldsBalancesUpdates(userWalletId, currencies), + flow2 = networksStatusesUpdates, + flow3 = networksStatusesUpdates.flatMapLatest { + val currenciesAddresses = it.getOrElse(default = { emptySet() }) + .mapNotNull { + val currency = currencies.firstOrNull { currency -> currency.network == it.network } + ?: return@mapNotNull null + + currency.id to extractAddress(it) + } + .toMap() + + getYieldsBalancesUpdates(userWalletId, currenciesAddresses) + }, flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -213,10 +230,14 @@ class CachedCurrenciesStatusesOperations( ) }, async { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( params = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = stakingIds, ), ) }, @@ -270,14 +291,17 @@ class CachedCurrenciesStatusesOperations( ): YieldBalance? { if (yieldBalances.isNullOrEmpty()) return null - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - - if (supportedIntegration.isNullOrBlank()) return null - + val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value val address = extractAddress(networkStatus) - return yieldBalances.firstOrNull { it.integrationId == supportedIntegration && it.address == address } - ?: YieldBalance.Error(integrationId = supportedIntegration, address = address) + return if (supportedIntegration != null && address != null) { + val stakingId = StakingID(integrationId = supportedIntegration, address = address) + + yieldBalances.firstOrNull { it.stakingId == stakingId } + ?: YieldBalance.Error(stakingId = stakingId) + } else { + null + } } private fun getCurrencies(userWalletId: UserWalletId): EitherFlow> { @@ -368,25 +392,24 @@ class CachedCurrenciesStatusesOperations( // temporary code because token list is built using networks list private fun getYieldsBalancesUpdates( userWalletId: UserWalletId, - cryptoCurrencies: List, + cryptoCurrencies: Map, ): EitherFlow> { return channelFlow { val state = MutableStateFlow(emptyList()) - cryptoCurrencies.onEach { + val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(currencyId = it.key, defaultAddress = it.value) + .getOrNull() + } + + stakingIds.onEach { stakingId -> launch { singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params( - userWalletId = userWalletId, - currencyId = it.id, - network = it.network, - ), + params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) .onEach { balance -> state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { - it.integrationId == balance.integrationId && it.address == balance.address - } + loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId } } } .launchIn(scope = this) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 82c396b43b..c1dc1ffb33 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -2,10 +2,10 @@ package com.tangem.domain.tokens.operations import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.staking.YieldBalance import java.math.BigDecimal internal class CurrencyStatusOperations( @@ -80,7 +80,8 @@ internal class CurrencyStatusOperations( val hasCurrentNetworkTransactions = networkStatusValue.pendingTransactions.isNotEmpty() val currentTransactions = networkStatusValue.pendingTransactions.getOrElse(currency.id, ::emptySet) val yieldBalanceData = yieldBalance as? YieldBalance.Data - val isCurrentAddressStaking = yieldBalanceData?.address == networkStatusValue.address.defaultAddress.value + val isCurrentAddressStaking = + yieldBalanceData?.stakingId?.address == networkStatusValue.address.defaultAddress.value val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter { it.token.coinGeckoId == currency.id.rawCurrencyId?.value } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index ec1e5a826d..198890fd63 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -3,10 +3,10 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.getResultStatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 0d853bbf2c..01eac8c1bb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -6,9 +6,9 @@ import arrow.core.raise.either import arrow.core.raise.withError import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 72382a701c..7669211249 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -9,12 +9,12 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList import com.tangem.utils.extensions.orZero import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 28522fdc9d..6dd77dfdc2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -3,11 +3,11 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.model.FeePaidCurrency import kotlinx.coroutines.flow.Flow /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt index a971f7d8be..8351958b10 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt @@ -1,12 +1,12 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning -import com.tangem.domain.models.wallet.UserWalletId import java.math.BigDecimal interface CurrencyChecksRepository { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index f6e74db9ca..bad702ce42 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -5,25 +5,21 @@ import arrow.core.NonEmptyList import arrow.core.raise.either import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalance.Unsupported.integrationId -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations /** * Proxy creator of [CryptoCurrencyStatus]. Used [CurrencyStatusOperations] to create statuses. * - * @property stakingRepository staking repository - * [REDACTED_AUTHOR] */ -class CurrencyStatusProxyCreator( - private val stakingRepository: StakingRepository, -) { +class CurrencyStatusProxyCreator { fun createCurrencyStatus( currency: CryptoCurrency, @@ -78,10 +74,13 @@ class CurrencyStatusProxyCreator( val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } val address = extractAddress(networkStatus) - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - val yieldBalance = if (supportedIntegration.isNullOrEmpty().not()) { - yieldBalances?.firstOrNull { it.integrationId == supportedIntegration && it.address == address } - ?: YieldBalance.Error(integrationId = supportedIntegration, address = address) + val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value + + val yieldBalance = if (supportedIntegration != null && address != null) { + val stakingId = StakingID(integrationId = supportedIntegration, address = address) + + yieldBalances?.firstOrNull { it.stakingId == stakingId } + ?: YieldBalance.Error(stakingId = stakingId) } else { null } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index 4e6677d2c0..bceb16c296 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -1,11 +1,14 @@ package com.tangem.domain.tokens.wallet import arrow.core.Either +import arrow.core.raise.either import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier @@ -13,7 +16,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -43,6 +45,7 @@ class WalletBalanceFetcher internal constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -54,6 +57,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( currenciesRepository = currenciesRepository, @@ -68,6 +72,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -145,13 +150,27 @@ class WalletBalanceFetcher internal constructor( private suspend fun fetchStaking( userWalletId: UserWalletId, currencies: Set, - ): Either { - return multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) + ): Either = either { + val maybeStakingIds = currencies.map { + val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it) + + if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { + Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${it.id}") + } + + stakingId + } + + val stakingIds = maybeStakingIds.mapNotNullTo(hashSetOf()) { it.getOrNull() } + + if (stakingIds.isNotEmpty()) { + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + ) + .bind() + } else { + Timber.i("No staking IDs found for user wallet $userWalletId with currencies: $currencies") + } } /** diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index a717323abe..8f3529b210 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -24,6 +24,7 @@ internal object MockNetworks { hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val network2 = Network( @@ -37,6 +38,7 @@ internal object MockNetworks { hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val network3 = Network( @@ -50,6 +52,7 @@ internal object MockNetworks { hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val verifiedNetworksStatuses: NonEmptySet diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt index d69fa589b0..6cc7313c8f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup @Suppress("MemberVisibilityCanBePrivate") internal object MockNetworksGroups { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index a380587679..caa2c13c6f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -5,11 +5,11 @@ import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index fddd277f37..555289ed3d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,11 +1,11 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.quote.fold -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index b408c3975e..8a0f5fa136 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -5,11 +5,11 @@ import arrow.core.getOrElse import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.model.FeePaidCurrency import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index d5d5f14816..1a8e1bf3f0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -1,20 +1,25 @@ package com.tangem.domain.tokens.wallet +import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.assertEither +import com.tangem.common.test.utils.assertEitherRight import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.wallet.FetchingSource.* import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest @@ -37,6 +42,7 @@ internal class WalletBalanceFetcherTest { private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( currenciesRepository = currenciesRepository, @@ -46,6 +52,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -83,6 +90,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -113,6 +121,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -146,6 +155,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -177,6 +187,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -222,6 +233,7 @@ internal class WalletBalanceFetcherTest { singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -267,6 +279,7 @@ internal class WalletBalanceFetcherTest { singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -281,7 +294,7 @@ internal class WalletBalanceFetcherTest { val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = setOf(ethereumStakingId, stellarStakingId), ) val exception = IllegalStateException("Error") @@ -289,6 +302,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns Either.Right(ethereumStakingId) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns Either.Right(stellarStakingId) coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() // Act @@ -305,6 +324,8 @@ internal class WalletBalanceFetcherTest { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } @@ -316,6 +337,131 @@ internal class WalletBalanceFetcherTest { } } + @Test + fun `fetch failure if stakingIdFactory RETURNS UnsupportedCurrency for all currencies`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) + } returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency) + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + assertEitherRight(actual) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if stakingIdFactory RETURNS UnableToGetAddress for all currencies`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val stakingId = Either.Left( + StakingIdFactory.Error.UnableToGetAddress(integrationId = StakingIntegrationID.EthereumToken.Polygon), + ) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + assertEitherRight(actual) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if stakingIdFactory RETURNS UnableToGetAddress and UnsupportedCurrency`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val ethereumStakingId = Either.Left( + StakingIdFactory.Error.UnableToGetAddress(integrationId = StakingIntegrationID.EthereumToken.Polygon), + ) + val stellarStakingId = Either.Left(StakingIdFactory.Error.UnsupportedCurrency) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns ethereumStakingId + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns stellarStakingId + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + assertEitherRight(actual) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + @Test fun `fetch failure if all fetching sources RETURNS LEFT`() = runTest { // Arrange @@ -337,7 +483,7 @@ internal class WalletBalanceFetcherTest { val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = setOf(ethereumStakingId, stellarStakingId), ) val exception = IllegalStateException("Error") @@ -347,6 +493,12 @@ internal class WalletBalanceFetcherTest { every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns Either.Right(ethereumStakingId) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns Either.Right(stellarStakingId) coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() // Act @@ -367,6 +519,8 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.fetchingSources multiNetworkStatusFetcher(params = networkStatusFetcherParams) multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } @@ -397,7 +551,7 @@ internal class WalletBalanceFetcherTest { val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = setOf(ethereumStakingId, stellarStakingId), ) every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver @@ -405,6 +559,12 @@ internal class WalletBalanceFetcherTest { every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns Either.Right(ethereumStakingId) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns Either.Right(stellarStakingId) coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns Unit.right() // Act @@ -420,6 +580,8 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.fetchingSources multiNetworkStatusFetcher(params = networkStatusFetcherParams) multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } @@ -475,6 +637,7 @@ internal class WalletBalanceFetcherTest { coVerify(inverse = true) { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -524,6 +687,7 @@ internal class WalletBalanceFetcherTest { coVerify(inverse = true) { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -531,5 +695,7 @@ internal class WalletBalanceFetcherTest { private companion object { val userWalletId = UserWalletId("011") + val ethereumStakingId = StakingID(integrationId = "ethereum", address = "0x1") + val stellarStakingId = StakingID(integrationId = "stellar", address = "0x1") } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt index 36f6ad081d..a6f1353437 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt @@ -10,14 +10,14 @@ import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.models.TwinKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet class PrepareForSendUseCase( private val transactionRepository: TransactionRepository, private val cardSdkConfigRepository: CardSdkConfigRepository, + private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner, ) { suspend operator fun invoke( transactionData: TransactionData, @@ -56,7 +56,13 @@ class PrepareForSendUseCase( } private fun createSigner(userWallet: UserWallet): TransactionSigner { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + return when (userWallet) { + is UserWallet.Hot -> getHotTransactionSigner(userWallet) + is UserWallet.Cold -> getColdSigner(userWallet) + } + } + + private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner { val card = userWallet.scanResponse.card val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt index ec98b2d43c..07b302c99c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.blockchain.common.TransactionSigner import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins @@ -11,25 +12,21 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.network.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet class SignUseCase( private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, + private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner, ) { suspend operator fun invoke( hash: ByteArray, userWallet: UserWallet, network: Network, ): Either { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - val card = userWallet.scanResponse.card - val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins - - val signer = cardSdkConfigRepository.getCommonSigner( - cardId = card.cardId.takeIf { isCardNotBackedUp }, - twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), - ) + val signer = when (userWallet) { + is UserWallet.Hot -> getHotTransactionSigner(userWallet) + is UserWallet.Cold -> getColdSigner(userWallet) + } val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) ?: error("WalletManager not found") @@ -39,4 +36,14 @@ class SignUseCase( is CompletionResult.Success -> signResult.data.right() } } + + private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + return cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt index 7ade9be75d..499e222199 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -6,8 +6,8 @@ import arrow.core.right import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.network.Network -import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.TransactionRepository class ValidateTransactionUseCase( private val transactionRepository: TransactionRepository, @@ -21,8 +21,8 @@ class ValidateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, - ): Either { - return transactionRepository.validateTransaction( + ): Either = Either.catch { + transactionRepository.validateTransaction( amount = amount, fee = fee, memo = memo, diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt index 17de705186..d6c97a1919 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt @@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable @Serializable data class VisaDataToSignByCustomerWallet( - val request: VisaCustomerWalletDataToSignRequest, val hashToSign: String, + val request: VisaCustomerWalletDataToSignRequest? = null, ) fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) = diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt new file mode 100644 index 0000000000..7c46288fc1 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaSignedChallengeByCustomerWallet( + val challenge: String, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt index e072914e92..7d46ff2d52 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -3,13 +3,12 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.pay.KycStartInfo -import com.tangem.domain.models.wallet.UserWalletId interface KycRepository { - suspend fun getKycStartInfo(): Either + suspend fun getKycStartInfo(address: String, cardId: String): Either interface Factory { - fun create(userWalletId: UserWalletId): KycRepository + fun create(): KycRepository } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index f7f19ca0e9..098ca44c00 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -18,6 +18,16 @@ interface VisaAuthRepository { cardWalletAddress: String, ): Either + suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either + + suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either + suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index e3f4cf7299..8b6bd34eae 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -16,7 +16,7 @@ import com.tangem.utils.extensions.mapNotNullValues sealed class WcAnalyticEvents( event: String, params: Map = mapOf(), -) : AnalyticsEvent(category = "Wallet Connect", event = event, params = params) { +) : AnalyticsEvent(category = WC_CATEGORY_NAME, event = event, params = params) { object ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( @@ -229,5 +229,6 @@ sealed class WcAnalyticEvents( companion object { const val NETWORKS = "Networks" const val DOMAIN_VERIFICATION = "Domain Verification" + const val WC_CATEGORY_NAME = "Wallet Connect" } } \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 8fbf7b49dd..a467663bf5 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -37,6 +37,11 @@ dependencies { implementation(tangemDeps.hot.core) // endregion + /** Other libraries */ + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.timber) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index ba55e47d78..eaa1c7dc7c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries + val allNetworks = Blockchain.entries.filter { it.isTestnet().not() } val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt new file mode 100644 index 0000000000..103f85021a --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO + +class ColdCurvesConfig(cardDTO: CardDTO) : CurvesConfig { + + val cardConfig = CardConfig.createConfig(cardDTO) + + override val mandatoryCurves: List + get() = cardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return cardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt new file mode 100644 index 0000000000..dcf753e3e3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.models.wallet.UserWallet + +interface CurvesConfig { + + val mandatoryCurves: List + + fun primaryCurve(blockchain: Blockchain): EllipticCurve? +} + +val UserWallet.curvesConfig: CurvesConfig + get() = when (this) { + is UserWallet.Cold -> ColdCurvesConfig(this.scanResponse.card) + is UserWallet.Hot -> HotCurvesConfig + } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt new file mode 100644 index 0000000000..eec380f63d --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.Wallet2CardConfig + +data object HotCurvesConfig : CurvesConfig { + + override val mandatoryCurves: List + get() = Wallet2CardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return Wallet2CardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index 16b0be7fa0..403ac7a76b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -10,11 +10,14 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class DefaultUserWalletsSyncDelegate( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val dispatchers: CoroutineDispatcherProvider, ) : UserWalletsSyncDelegate { @@ -28,10 +31,43 @@ class DefaultUserWalletsSyncDelegate( } } - // TODO remove dispatchers whnen UserWalletsListManager will be main safe private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, + ): Either = if (useNewRepository) { + renameUserWalletInNewRepository(userWalletId, name) + } else { + renameUserWalletInLegacyRepository(userWalletId, name) + } + + private suspend fun renameUserWalletInNewRepository( + userWalletId: UserWalletId, + name: String, + ): Either = either { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.find { it.walletId == userWalletId } + ?: raise(UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found"))) + + ensure(userWallets.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + ensure(name != userWallet.name) { + UpdateWalletError.NameAlreadyExists + } + + val updatedWallet = userWallet.copy(name = name) + + userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .map { updatedWallet } + .mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("")) } + .bind() + } + + // TODO remove dispatchers whnen UserWalletsListManager will be main safe + private suspend fun renameUserWalletInLegacyRepository( + userWalletId: UserWalletId, + name: String, ): Either = withContext(dispatchers.io) { either { val existingNames = userWalletsListManager.userWalletsSync diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt new file mode 100644 index 0000000000..86da09c677 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.wallets.derivations + +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +interface ColdMapDerivationsRepository { + + @Throws + suspend fun derivePublicKeys(userWallet: UserWallet.Cold, currencies: List): UserWallet.Cold + + suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Cold, + networkIds: List, + ): UserWallet.Cold + + @Throws + suspend fun derivePublicKeysByNetworks(userWallet: UserWallet.Cold, networks: List): UserWallet.Cold + + @Throws + suspend fun derivePublicKeys( + userWallet: UserWallet.Cold, + derivations: Map>, + ): Pair> + + /** Check if user [userWallet] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend fun hasMissedDerivations( + userWallet: UserWallet.Cold, + networksWithDerivationPath: Map, + ): Boolean +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivationStyleProvider.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProvider.kt similarity index 92% rename from domain/card/src/main/kotlin/com/tangem/domain/card/DerivationStyleProvider.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProvider.kt index 36267e8d35..51e7ee6763 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivationStyleProvider.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.card +package com.tangem.domain.wallets.derivations import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.domain.card.common.TapWorkarounds.isWallet2 @@ -25,7 +25,6 @@ internal class TangemDerivationStyleProvider( } } -// TODO remove this class [REDACTED_TASK_KEY] internal class TangemHotDerivationStyleProvider : DerivationStyleProvider { override fun getDerivationStyle(): DerivationStyle? = DerivationStyle.V3 } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt new file mode 100644 index 0000000000..6525c2d528 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.derivations + +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet + +val UserWallet.derivationStyleProvider: DerivationStyleProvider + get() = when (this) { + is UserWallet.Cold -> scanResponse.derivationStyleProvider + is UserWallet.Hot -> TangemHotDerivationStyleProvider() + } + +val ScanResponse.derivationStyleProvider: DerivationStyleProvider + get() = card.derivationStyleProvider + +val CardDTO.derivationStyleProvider: DerivationStyleProvider + get() = TangemDerivationStyleProvider(this) \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt similarity index 92% rename from domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt index 0f655df789..43b5190947 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt @@ -1,11 +1,11 @@ -package com.tangem.domain.card.repository +package com.tangem.domain.wallets.derivations import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.BackendId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap interface DerivationsRepository { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt new file mode 100644 index 0000000000..65364e9a80 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.wallets.derivations + +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +interface HotMapDerivationsRepository { + + @Throws + suspend fun derivePublicKeys(userWallet: UserWallet.Hot, currencies: List): UserWallet.Hot + + suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Hot, + networkIds: List, + ): UserWallet.Hot + + @Throws + suspend fun derivePublicKeysByNetworks(userWallet: UserWallet.Hot, networks: List): UserWallet.Hot + + @Throws + suspend fun derivePublicKeys( + userWallet: UserWallet.Hot, + derivations: Map>, + ): Pair> + + /** Check if user [userWallet] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend fun hasMissedDerivations( + userWallet: UserWallet.Hot, + networksWithDerivationPath: Map, + ): Boolean +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt index 99348982ad..74055cd741 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt @@ -4,16 +4,14 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.common.util.hasDerivation -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.wallet.UserWallet -import kotlin.collections.first -import kotlin.collections.orEmpty +import com.tangem.domain.wallets.config.curvesConfig fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Boolean { return when (this) { is UserWallet.Cold -> scanResponse.hasDerivation(blockchain, derivationPath) is UserWallet.Hot -> { - val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) // TODO [REDACTED_TASK_KEY]: handle hot wallet config + val primaryCurve = curvesConfig.primaryCurve(blockchain) val list = if (blockchain == Blockchain.Cardano) { listOf( CardanoUtils.extendedDerivationPath(DerivationPath(derivationPath)), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..48c37f6440 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.wallets.hot + +import com.tangem.hot.sdk.model.HotWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository for managing access code attempts for hot wallets. + * It tracks the number of attempts made to access a hot wallet and applies cooldowns or deletion + * based on the number of attempts. + */ +interface HotWalletAccessCodeAttemptsRepository { + + /** + * Increments the number of attempts for the given [AttemptId]. + * If the number of attempts exceeds [MAX_FAST_FORWARD_ATTEMPTS], a cooldown period is initiated. + */ + suspend fun incrementAttempts(id: AttemptId) + + /** + * Resets the attempts for the given [HotWalletId]. + * This is typically called when the user successfully authenticates or when the wallet is deleted. + */ + suspend fun resetAttempts(hotWalletId: HotWalletId) + + /** + * Retrieves the current attempts for the given [AttemptId]. + * The result is a flow that emits the current state of attempts. + */ + fun getAttempts(id: AttemptId): Flow + + /** + * Synchronously retrieves the current attempts for the given [AttemptId]. + * This is useful when you need to get the attempts without using a flow. + */ + suspend fun getAttemptsSync(id: AttemptId): Attempts + + data class AttemptId( + val hotWalletId: HotWalletId, + val auth: Boolean, + ) + + sealed interface Attempts { + val count: Int + + data class FastForward( + override val count: Int, + ) : Attempts + + data class WithDelay( + override val count: Int, + val remainingSeconds: Int, + ) : Attempts + + data class BeforeDeletion( + override val count: Int, + val remainingSeconds: Int, + val remainingAttemptsCountBeforeDeletion: Int, + ) : Attempts + + data object Deletion : Attempts { + override val count: Int = MAX_ATTEMPTS_BEFORE_DELETION + } + } + + companion object { + const val COOLDOWN_SECONDS = 60 + const val MAX_FAST_FORWARD_ATTEMPTS = 5 + const val ATTEMPTS_BEFORE_DELETION = 20 + const val MAX_ATTEMPTS_BEFORE_DELETION = 30 + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt new file mode 100644 index 0000000000..c3f3df8d95 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -0,0 +1,52 @@ +package com.tangem.domain.wallets.hot + +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId + +/** + * Interface for requesting the password for a hot wallet. + * It provides methods to handle password requests, authentication states, and user interactions. + */ +interface HotWalletPasswordRequester { + + /** + * Sets state to show wrong password state. + */ + suspend fun wrongPassword() + + /** + * Sets state to show successful authentication state. + */ + suspend fun successfulAuthentication() + + /** + * Requests the user to enter the password for the hot wallet. + * @param attemptRequest Contains information about the hot wallet and authentication mode. + * @return Result of the password request, which can be either a password entry, biometric use, or dismissal. + */ + suspend fun requestPassword(attemptRequest: AttemptRequest): Result + + /** + * Dismisses the password request dialog. + */ + suspend fun dismiss() + + /** + * Represents a request to authenticate with a hot wallet. + * @param hotWalletId The ID of the hot wallet to authenticate with. + * @param authMode Indicates whether the request is for authentication mode. + * In auth mode user can be deleted after failed attempts. + * @param hasBiometry Indicates whether to show biometric authentication option. + */ + data class AttemptRequest( + val hotWalletId: HotWalletId, + val authMode: Boolean, + val hasBiometry: Boolean, + ) + + sealed class Result { + data object UseBiometry : Result() + data object Dismiss : Result() + data class EnteredPassword(val password: HotAuth.Password) : Result() + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt deleted file mode 100644 index e2aeab608f..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed interface SelectWalletError { - - object UnableToSelectUserWallet : SelectWalletError -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt deleted file mode 100644 index 5616695e03..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.wallets.repository - -import com.tangem.domain.models.network.Network - -interface HotDerivationsRepository { - - fun getAllSupportedNetworks(): Set -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 5a6c051057..a1eb1f0cd8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -11,10 +11,20 @@ interface WalletsRepository { suspend fun shouldSaveUserWalletsSync(): Boolean + @Deprecated("Hot wallet make always save user wallets. Do not use this method") fun shouldSaveUserWallets(): Flow + @Deprecated("Hot wallet make always save user wallets. Do not use this method") suspend fun saveShouldSaveUserWallets(item: Boolean) + suspend fun useBiometricAuthentication(): Boolean + + suspend fun setUseBiometricAuthentication(value: Boolean) + + suspend fun requireAccessCode(): Boolean + + suspend fun setRequireAccessCode(value: Boolean) + suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index 65faf81196..c3c49c4f7e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -4,8 +4,9 @@ import arrow.core.Either import arrow.core.raise.either import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.DeleteWalletError +import com.tangem.domain.core.wallets.error.DeleteWalletError import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for deleting user wallet @@ -14,7 +15,11 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class DeleteWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { /** * Deletes user wallet with provided ID. @@ -24,6 +29,12 @@ class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListMan * @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. * */ suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map { + userWalletsListRepository.selectedUserWallet.value != null + } + } + return either { userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) .doOnFailure { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DerivePublicKeysUseCase.kt similarity index 74% rename from domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DerivePublicKeysUseCase.kt index 86ed75fe6d..164d43a5cf 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DerivePublicKeysUseCase.kt @@ -1,16 +1,17 @@ -package com.tangem.domain.card + +package com.tangem.domain.wallets.usecase import arrow.core.Either -import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.derivations.DerivationsRepository class DerivePublicKeysUseCase( private val derivationsRepository: DerivationsRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, currencies: List): Either { - return Either.catch { + return Either.Companion.catch { derivationsRepository.derivePublicKeys(userWalletId, currencies) } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt new file mode 100644 index 0000000000..7c80707cb6 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.wallets.usecase + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class GenerateBuyTangemCardLinkUseCase { + + suspend operator fun invoke(): String = suspendCoroutine { cont -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { id -> + cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id") + } + .addOnFailureListener { + cont.resume(NEW_BUY_WALLET_URL) + } + } + + companion object { + private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt index e779085950..fdc88856e7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -2,12 +2,16 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.scan.ProductType import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for user wallet name generation */ class GenerateWalletNameUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { @@ -17,16 +21,24 @@ class GenerateWalletNameUseCase( isStartToCoin = isStartToCoin, ) - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } fun invokeForHot(): String { val defaultName = "Wallet" - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } + private fun getNamesSet(): Set { + return if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet() + } else { + userWalletsListManager.userWalletsSync.map { it.name }.toSet() + } + } + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { val startIndex = 2 if (!existingNames.contains(defaultName)) { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt similarity index 98% rename from domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt index ebbd38f355..e548eef8b7 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.card +package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.right @@ -10,10 +10,10 @@ import com.tangem.common.extensions.calculateSha256 import com.tangem.crypto.NetworkType import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.operations.derivation.ExtendedPublicKeysMap /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt index 6afc79d552..32233cdbf9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt @@ -4,13 +4,20 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.* class GetSavedWalletsCountUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(): Flow> { + if (useNewRepository) { + return userWalletsListRepository.userWallets.map { requireNotNull(it) } + } + return userWalletsListManager.savedWalletsCount .filter { count -> if (count == 0) return@filter true diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index 00451b45de..5b681fb679 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for getting selected wallet. @@ -15,10 +16,20 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetSelectedWalletSyncUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either { + if (useNewRepository) { + return either { + userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound) + } + } + return either { ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index d479a9d59b..57a01d23fb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -5,7 +5,9 @@ import arrow.core.raise.either import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull /** * Use case for getting flow of selected wallet. @@ -14,12 +16,32 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") +class GetSelectedWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either> { return either { - userWalletsListManager.selectedUserWallet + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.filterNotNull() + } else { + userWalletsListManager.selectedUserWallet + } + } + } + + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") + fun sync(): Either { + return either { + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.value + } else { + userWalletsListManager.selectedUserWalletSync + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index b1a548af9a..6f4e13f0f0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -10,13 +10,24 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest -class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetUserWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWallets = userWalletsListManager.userWalletsSync + val userWallets = if (useNewListRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) @@ -25,7 +36,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa @OptIn(ExperimentalCoroutinesApi::class) fun invokeFlow(userWalletId: UserWalletId): EitherFlow { - return userWalletsListManager.userWallets.transformLatest { userWallets -> + val flow = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } + + return flow.transformLatest { userWallets -> userWallets.firstOrNull { it.walletId == userWalletId } ?.let { emit(it.right()) } ?: emit(GetUserWalletError.UserWalletNotFound.left()) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt index 0108e03b67..377bf1b152 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -1,13 +1,23 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for getting list of user wallets names. * * @property userWalletsListManager user wallets list manager */ -class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletNamesUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { - operator fun invoke(): List = userWalletsListManager.userWalletsSync.map { it.name } + operator fun invoke(): List = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name } + } else { + userWalletsListManager.userWalletsSync.map { it.name } + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 7e6a0b6510..6635d63099 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -1,8 +1,10 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Use case for getting list of user wallets @@ -11,11 +13,23 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletsUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = userWalletsListManager.userWallets + operator fun invoke(): Flow> = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } @Throws(IllegalArgumentException::class) - fun invokeSync(): List = userWalletsListManager.userWalletsSync + fun invokeSync(): List = if (useNewListRepository) { + userWalletsListRepository.userWallets.value!! + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasMissedDerivationsUseCase.kt similarity index 75% rename from domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasMissedDerivationsUseCase.kt index 69aa1442c2..c9b008133b 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasMissedDerivationsUseCase.kt @@ -1,22 +1,20 @@ -package com.tangem.domain.card +package com.tangem.domain.wallets.usecase -import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.derivations.DerivationsRepository + +typealias BackendId = String /** * Use case to check if user has missed derivations * [REDACTED_AUTHOR] */ - -typealias BackendId = String - class HasMissedDerivationsUseCase( private val derivationsRepository: DerivationsRepository, ) { - /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ + /** Check if user [userWalletId] has missed derivations using map of [com.tangem.domain.models.network.Network.ID] with extraDerivationPath */ suspend operator fun invoke( userWalletId: UserWalletId, networksWithDerivationPath: Map, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt index 29240ff71b..05d47b70d0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,12 +13,22 @@ import kotlinx.coroutines.flow.map * * @property userWalletsListManager user wallets list manager */ -class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) { +class IsNeedToBackupUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { operator fun invoke(id: UserWalletId): Flow { - return userWalletsListManager.userWallets + val userWalletsFlow = if (useNewRepository) { + userWalletsListRepository.userWallets + } else { + userWalletsListManager.userWallets + } + + return userWalletsFlow .map { wallets -> - val wallet = wallets.firstOrNull { it.walletId == id } + val wallet = wallets?.firstOrNull { it.walletId == id } if (wallet == null) { false } else { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 66777ba22b..7e328ae146 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -6,10 +6,12 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.SaveWalletError -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository /** * Use case for saving user wallet @@ -18,22 +20,60 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class SaveWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { - return either { - userWalletsListManager.save(userWallet, canOverride) - .doOnSuccess { return Unit.right() } - .doOnFailure { - return when (it) { - is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( - it.messageResId, - ) - else -> SaveWalletError.DataError(it.messageResId) - }.left() - } + return if (useNewRepository) { + either { + val newUserWallet = + userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() - return Unit.right() + if (newUserWallet) { + when (userWallet) { + is UserWallet.Cold -> { + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } else { + Unit.right() + } + } + is UserWallet.Hot -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.NoLock, + ) + } + }.mapLeft { + SaveWalletError.DataError(null) + }.map { + userWalletsListRepository.select(userWallet.walletId) + }.bind() + } + } + } else { + either { + userWalletsListManager.save(userWallet, canOverride) + .doOnSuccess { return Unit.right() } + .doOnFailure { + return when (it) { + is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( + it.messageResId, + ) + else -> SaveWalletError.DataError(it.messageResId) + }.left() + } + + return Unit.right() + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 3ff5b201d7..21a6a76755 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -4,11 +4,12 @@ import arrow.core.Either import arrow.core.raise.either import arrow.core.right import com.tangem.common.CompletionResult +import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for selecting wallet @@ -20,10 +21,19 @@ import com.tangem.domain.models.wallet.UserWalletId */ class SelectWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.select(userWalletId).map { + reduxStateHolder.onUserWalletSelected(it) + it + } + } + return either { return when (val result = userWalletsListManager.select(userWalletId)) { is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index b6d0accf29..96c2f19f7a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -7,6 +7,9 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.wallets.models.UpdateWalletError.* +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for updating user wallet @@ -15,15 +18,38 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class UpdateWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, - ): Either = either { - when (val result = userWalletsListManager.update(userWalletId, update)) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data + ): Either { + if (useNewRepository) { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + ?: return Either.Left( + UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found")), + ) + val updatedWallet = update(userWallet) + return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .mapLeft { + when (it) { + is SaveWalletError.DataError -> DataError( + IllegalStateException("Failed to update wallet: ${it.messageId}"), + ) + is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists + } + } + } + + return either { + when (val result = userWalletsListManager.update(userWalletId, update)) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt index b71be78c5e..4df283dd5e 100644 --- a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt @@ -22,7 +22,11 @@ class GetSavedWalletsCountUseCaseTest { @Before fun setup() { userWalletsListManager = mockk() - useCase = GetSavedWalletsCountUseCase(userWalletsListManager) + useCase = GetSavedWalletsCountUseCase( + userWalletsListManager, + userWalletsListRepository = mockk(), + useNewRepository = false, + ) mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") } diff --git a/features/account/api/.gitignore b/features/account/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/account/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/account/api/build.gradle.kts b/features/account/api/build.gradle.kts new file mode 100644 index 0000000000..e3e35ac4cd --- /dev/null +++ b/features/account/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.account.api" +} + +dependencies { + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Project - Domain */ + implementation(projects.domain.models) +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt new file mode 100644 index 0000000000..b18a7db430 --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWalletId + +interface AccountCreateEditComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + sealed interface Params { + + data class Create( + val userWalletId: UserWalletId, + ) : Params + + data class Edit( + val account: Account, + ) : Params + } +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt new file mode 100644 index 0000000000..d1c47280ea --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.account.Account + +interface AccountDetailsComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params(val account: Account) +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt new file mode 100644 index 0000000000..91acb0ea4d --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface ArchivedAccountListComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/account/impl/.gitignore b/features/account/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/account/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts new file mode 100644 index 0000000000..dc77a38d28 --- /dev/null +++ b/features/account/impl/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.account.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.account.api) + + /** Core modules */ + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.error) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.account) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt new file mode 100644 index 0000000000..3c2aed7f6b --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -0,0 +1,68 @@ +package com.tangem.features.account.archived + +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("UnusedPrivateMember") // todo account +internal class ArchivedAccountListModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow get() = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun confirmRecoverDialog(accountId: AccountId) { + val account: Account? = null // todo account find + account ?: return + val secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_archived_recover), + onClick = { recoverCryptoPortfolio(account.accountId) }, + ) + messageSender.send( + DialogMessage( + title = stringReference(account.name.value), + message = TextReference.EMPTY, + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + recoverCryptoPortfolioUseCase(accountId) + } + + private fun getInitialState(): AccountArchivedUM { + return AccountArchivedUM.Loading( + onCloseClick = { router.pop() }, + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt new file mode 100644 index 0000000000..6179fd12b2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.archived + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ui.ArchivedAccountListContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultArchivedAccountListComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: ArchivedAccountListComponent.Params, +) : AppComponentContext by appComponentContext, ArchivedAccountListComponent { + + private val model: ArchivedAccountListModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ArchivedAccountListContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : ArchivedAccountListComponent.Factory { + override fun create( + context: AppComponentContext, + params: ArchivedAccountListComponent.Params, + ): DefaultArchivedAccountListComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt new file mode 100644 index 0000000000..21c674cef2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ArchivedAccountListModel +import com.tangem.features.account.archived.DefaultArchivedAccountListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountArchivedModule { + + @Binds + fun bindArchivedAccountListComponentFactory( + impl: DefaultArchivedAccountListComponent.Factory, + ): ArchivedAccountListComponent.Factory + + @Binds + @IntoMap + @ClassKey(ArchivedAccountListModel::class) + fun bindArchivedAccountListModel(model: ArchivedAccountListModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt new file mode 100644 index 0000000000..ee42b871ff --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.entity + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.account.common.CryptoPortfolioIconUM +import kotlinx.collections.immutable.ImmutableList + +internal sealed interface AccountArchivedUM { + val onCloseClick: () -> Unit + + data class Loading(override val onCloseClick: () -> Unit) : AccountArchivedUM + data class Error( + override val onCloseClick: () -> Unit, + val onRetryClick: () -> Unit, + ) : AccountArchivedUM + data class Content( + override val onCloseClick: () -> Unit, + val accounts: ImmutableList, + ) : AccountArchivedUM +} + +internal data class ArchivedAccountUM( + val accountId: String, + val accountName: String, + val accountIcon: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val onClick: (accountId: String) -> Unit, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt new file mode 100644 index 0000000000..65b5e1794c --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -0,0 +1,202 @@ +package com.tangem.features.account.archived.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.features.account.archived.entity.ArchivedAccountUM +import com.tangem.features.account.common.toUM +import com.tangem.features.account.details.ui.AccountIcon +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun ArchivedAccountListContent(state: AccountArchivedUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = stringResourceSafe(R.string.account_archived_title), + onBackClick = state.onCloseClick, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + + ) { + when (state) { + is AccountArchivedUM.Content -> ArchiveAccountContent(state) + is AccountArchivedUM.Error -> ArchiveAccountError(state) + is AccountArchivedUM.Loading -> ArchiveAccountLoading() + } + } + } +} + +@Composable +private fun ArchiveAccountLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + modifier = Modifier, + ) + } +} + +@Composable +private fun ArchiveAccountError(state: AccountArchivedUM.Error, modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = stringResourceSafe(R.string.common_unable_to_load), + ) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = state.onRetryClick, + ), + ) + } + } +} + +@Composable +private fun ArchiveAccountContent(state: AccountArchivedUM.Content, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = state.accounts, + key = { index, item -> item.accountId }, + ) { index, account -> + ArchivedAccountRow( + item = account, + modifier = Modifier.roundedShapeItemDecoration( + backgroundColor = TangemTheme.colors.background.primary, + radius = TangemTheme.dimens.radius20, + currentIndex = index, + addDefaultPadding = true, + lastIndex = state.accounts.lastIndex, + ), + ) + } + } +} + +@Composable +private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = { item.onClick(item.accountId) }) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)), + accountName = item.accountName, + accountIcon = item.accountIcon, + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = item.accountName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = item.tokensInfo.resolveReference(), + ) + } + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.account_archived_recover), + onClick = { item.onClick(item.accountId) }, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountArchivedUM) { + TangemThemePreview { + ArchivedAccountListContent(state = params) + } +} + +@Suppress("MagicNumber") +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + + val firstList = List(10) { + ArchivedAccountUM( + accountId = it.toString(), + accountName = "Account name", + accountIcon = portfolioIcon(), + tokensInfo = stringReference("10 tokens in 2 networks"), + onClick = {}, + + ) + }.toImmutableList() + val first = AccountArchivedUM.Content( + onCloseClick = {}, + accounts = firstList, + ) + add(first) + add(AccountArchivedUM.Loading {}) + add(AccountArchivedUM.Error({}, {})) + }, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt b/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt new file mode 100644 index 0000000000..299fb679dc --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt @@ -0,0 +1,18 @@ +package com.tangem.features.account.common + +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color +import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon + +data class CryptoPortfolioIconUM( + val value: Icon, + val color: Color, +) { + constructor(domainModel: CryptoPortfolioIcon) : this( + value = domainModel.value, + color = domainModel.color, + ) +} + +fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this) +fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt new file mode 100644 index 0000000000..0f5a66dedf --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -0,0 +1,195 @@ +package com.tangem.features.account.createedit + +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.utils.showErrorDialog +import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.common.toDomain +import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateDerivationIndex +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateIconSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName +import com.tangem.features.account.createedit.error.AccountFeatureError +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ModelScoped +@Suppress("LongParameterList") +internal class AccountCreateEditModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + private val updateCryptoPortfolioUseCase: UpdateCryptoPortfolioUseCase, + private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, + private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, +) : Model() { + + private val params = paramsContainer.require() + private val umBuilder = AccountCreateEditUMBuilder(params) + + val uiState: StateFlow + field = MutableStateFlow(value = getInitialState()) + + init { + if (params is AccountCreateEditComponent.Params.Create) { + updateDerivationInfo(userWalletId = params.userWalletId) + } + } + + private fun unsaveChangeDialog() { + val secondAction = EventMessageAction( + title = resourceReference(R.string.account_unsaved_dialog_action_first), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_unsaved_dialog_action_second), + warning = true, + onClick = { router.pop() }, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_unsaved_dialog_title), + message = resourceReference(R.string.account_unsaved_dialog_message_create), + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun onConfirmClick() = modelScope.launch { + when (params) { + is AccountCreateEditComponent.Params.Create -> createNewCryptoPortfolio(params) + is AccountCreateEditComponent.Params.Edit -> editCryptoPortfolio(params) + } + } + + private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) { + val state = uiState.value + val name = AccountName(value = state.account.name).getOrNull() ?: return + val icon = state.account.portfolioIcon.toDomain() + val index = state.account.derivationInfo.index ?: return + val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return + + addCryptoPortfolioUseCase( + userWalletId = params.userWalletId, + accountName = name, + icon = icon, + derivationIndex = derivationIndex, + ) + } + + private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) { + val state = uiState.value + val name = AccountName(state.account.name).getOrNull() ?: return + val icon = state.account.portfolioIcon.toDomain() + val isNewName = name != params.account.name + val isNewIcon = icon != params.account.portfolioIcon + updateCryptoPortfolioUseCase( + icon = if (isNewIcon) icon else null, + accountName = if (isNewName) name else null, + accountId = params.account.accountId, + ) + } + + private fun onCloseClick() = unsaveChangeDialog() + + private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) { + uiState.value = uiState.value + .updateIconSelect(icon) + .validateNewState() + } + + private fun onColorSelect(color: CryptoPortfolioIcon.Color) { + uiState.value = uiState.value + .updateColorSelect(color) + .validateNewState() + } + + private fun onNameChange(name: String) { + uiState.value = uiState.value + .updateName(name) + .validateNewState() + } + + private fun AccountCreateEditUM.validateNewState(): AccountCreateEditUM { + val isValidName = AccountName(this.account.name).isRight() + val isAvailableForConfirm = when (params) { + is AccountCreateEditComponent.Params.Create -> isValidName + is AccountCreateEditComponent.Params.Edit -> { + val isNewName = this.account.name != params.account.name.value + val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon + isValidName && (isNewName || isNewIcon) + } + } + return this.updateButton(isButtonEnabled = isAvailableForConfirm) + } + + private fun getInitialState(): AccountCreateEditUM { + return AccountCreateEditUM( + title = umBuilder.toolbarTitle, + account = umBuilder.initAccountUM(::onNameChange), + colorsState = umBuilder.initColorsUM(::onColorSelect), + iconsState = umBuilder.initIconsUM(::onIconSelect), + buttonState = umBuilder.initButtonUM(::onConfirmClick), + onCloseClick = ::onCloseClick, + ) + } + + private fun updateDerivationInfo(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.default) { + getUnoccupiedAccountIndexUseCase(userWalletId = userWalletId) + .onRight { derivationIndex -> + uiState.update { + it.updateDerivationIndex(derivationIndex = derivationIndex.value) + } + } + .onLeft { + handleError( + error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, + params = mapOf("userWalletId" to userWalletId.stringValue), + ) + + return@launch + } + } + } + + private fun handleError(error: AccountFeatureError, params: Map = mapOf()) { + val exception = IllegalStateException(error.toString()) + + Timber.e(exception) + + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent(exception = exception, params = params), + ) + + messageSender.showErrorDialog(universalError = error, onDismiss = router::pop) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt new file mode 100644 index 0000000000..e920c6cbbe --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.createedit + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.ui.AccountCreateEditContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAccountCreateEditComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: AccountCreateEditComponent.Params, +) : AppComponentContext by appComponentContext, AccountCreateEditComponent { + + private val model: AccountCreateEditModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AccountCreateEditContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : AccountCreateEditComponent.Factory { + override fun create( + context: AppComponentContext, + params: AccountCreateEditComponent.Params, + ): DefaultAccountCreateEditComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt new file mode 100644 index 0000000000..ab899b3f5c --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.createedit.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.AccountCreateEditModel +import com.tangem.features.account.createedit.DefaultAccountCreateEditComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountCreateEditModule { + + @Binds + fun bindAccountCreateEditComponentFactory( + impl: DefaultAccountCreateEditComponent.Factory, + ): AccountCreateEditComponent.Factory + + @Binds + @IntoMap + @ClassKey(AccountCreateEditModel::class) + fun bindAccountCreateEditModel(model: AccountCreateEditModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt new file mode 100644 index 0000000000..df93dde4b9 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -0,0 +1,54 @@ +package com.tangem.features.account.createedit.entity + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.common.CryptoPortfolioIconUM +import kotlinx.collections.immutable.ImmutableList + +data class AccountCreateEditUM( + val title: TextReference, + val account: Account, + val colorsState: Colors, + val iconsState: Icons, + val buttonState: Button, + val onCloseClick: () -> Unit, +) { + + data class Account( + val name: String, + val portfolioIcon: CryptoPortfolioIconUM, + val derivationInfo: DerivationInfo, + val inputPlaceholder: TextReference, + val onNameChange: (String) -> Unit, + ) + + sealed interface DerivationInfo { + val text: TextReference + val index: Int? + + data class Content(override val text: TextReference, override val index: Int) : DerivationInfo + + data object Empty : DerivationInfo { + override val text: TextReference = TextReference.EMPTY + override val index: Int? = null + } + } + + data class Colors( + val selected: CryptoPortfolioIcon.Color, + val list: ImmutableList, + val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit, + ) + + data class Icons( + val selected: CryptoPortfolioIcon.Icon, + val list: ImmutableList, + val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit, + ) + + data class Button( + val isButtonEnabled: Boolean, + val onConfirmClick: () -> Unit, + val text: TextReference, + ) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt new file mode 100644 index 0000000000..bacbd306ab --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -0,0 +1,139 @@ +package com.tangem.features.account.createedit.entity + +import com.tangem.core.res.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.models.account.Account +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.common.toUM +import kotlinx.collections.immutable.toImmutableList + +internal class AccountCreateEditUMBuilder( + private val params: AccountCreateEditComponent.Params, +) { + + private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList() + private val accountIcons = CryptoPortfolioIcon.Icon.entries.toImmutableList() + private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + + val toolbarTitle: TextReference + get() = when (params) { + is AccountCreateEditComponent.Params.Create -> resourceReference(R.string.account_form_title_create) + is AccountCreateEditComponent.Params.Edit -> resourceReference(R.string.account_form_title_edit) + } + + fun initAccountUM(onNameChange: (String) -> Unit): AccountCreateEditUM.Account { + return when (params) { + is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account( + name = "", + portfolioIcon = createIcon, + derivationInfo = AccountCreateEditUM.DerivationInfo.Empty, + inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account), + onNameChange = onNameChange, + ) + is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( + name = params.account.name.value, + portfolioIcon = params.account.portfolioIcon.toUM(), + derivationInfo = createAccountDerivationInfo( + index = (params.account as Account.CryptoPortfolio).derivationIndex.value, + ), + inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), + onNameChange = onNameChange, + ) + } + } + + fun initColorsUM(onColorSelect: (CryptoPortfolioIcon.Color) -> Unit): AccountCreateEditUM.Colors { + val selected: CryptoPortfolioIcon.Color = when (params) { + is AccountCreateEditComponent.Params.Create -> createIcon.color + is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.color + } + return AccountCreateEditUM.Colors( + selected = selected, + list = accountColors, + onColorSelect = onColorSelect, + ) + } + + fun initIconsUM(onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit): AccountCreateEditUM.Icons { + val selected: CryptoPortfolioIcon.Icon = when (params) { + is AccountCreateEditComponent.Params.Create -> createIcon.value + is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.value + } + return AccountCreateEditUM.Icons( + selected = selected, + list = accountIcons, + onIconSelect = onIconSelect, + ) + } + + fun initButtonUM(onConfirmClick: () -> Unit): AccountCreateEditUM.Button { + val text: TextReference = when (params) { + is AccountCreateEditComponent.Params.Create -> resourceReference(R.string.account_form_create_button) + is AccountCreateEditComponent.Params.Edit -> resourceReference(R.string.account_form_edit_button) + } + return AccountCreateEditUM.Button( + isButtonEnabled = false, + onConfirmClick = onConfirmClick, + text = text, + ) + } + + internal companion object { + + val Account.portfolioIcon: CryptoPortfolioIcon + get() = when (this) { + is Account.CryptoPortfolio -> this.icon + } + + fun AccountCreateEditUM.updateColorSelect(color: CryptoPortfolioIcon.Color): AccountCreateEditUM { + val newIcon = this.account.portfolioIcon.copy( + color = color, + ) + return this.copy( + account = this.account.copy(portfolioIcon = newIcon), + colorsState = this.colorsState.copy(selected = color), + ) + } + + fun AccountCreateEditUM.updateIconSelect(icon: CryptoPortfolioIcon.Icon): AccountCreateEditUM { + val newIcon = this.account.portfolioIcon.copy( + value = icon, + ) + return this.copy( + account = this.account.copy(portfolioIcon = newIcon), + iconsState = this.iconsState.copy(selected = icon), + ) + } + + fun AccountCreateEditUM.updateName(name: String): AccountCreateEditUM { + return this.copy(account = this.account.copy(name = name)) + } + + fun AccountCreateEditUM.updateButton(isButtonEnabled: Boolean): AccountCreateEditUM { + return this.copy(buttonState = this.buttonState.copy(isButtonEnabled = isButtonEnabled)) + } + + fun AccountCreateEditUM.updateDerivationIndex(derivationIndex: Int): AccountCreateEditUM { + return this.copy( + account = this.account.copy( + derivationInfo = createAccountDerivationInfo(index = derivationIndex), + ), + ) + } + + private fun createAccountDerivationInfo(index: Int): AccountCreateEditUM.DerivationInfo { + val derivationIndexText = if (index.toString().length == 1) "0$index" else "$index" + + return AccountCreateEditUM.DerivationInfo.Content( + text = resourceReference( + id = R.string.account_form_account_index, + formatArgs = wrappedList(derivationIndexText), + ), + index = index, + ) + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt new file mode 100644 index 0000000000..9ab8549fc7 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt @@ -0,0 +1,30 @@ +package com.tangem.features.account.createedit.error + +import com.tangem.core.error.UniversalError + +sealed interface AccountFeatureError : UniversalError { + + val subsystemCode: String + val specificErrorCode: String + + override val errorCode: Int + get() = "108$subsystemCode$specificErrorCode".toInt() + + sealed interface CreateAccount : AccountFeatureError { + + override val subsystemCode: String get() = "001" + + data object UnableToGetDerivationIndex : CreateAccount { + override val specificErrorCode: String = "001" + } + } + + sealed interface EditAccount : AccountFeatureError { + + override val subsystemCode: String get() = "002" + + data object RequiredCryptoPortfolio : EditAccount { + override val specificErrorCode: String = "001" + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt new file mode 100644 index 0000000000..b94e9198fe --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -0,0 +1,366 @@ +package com.tangem.features.account.createedit.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.account.getResId +import com.tangem.common.ui.account.getUiColor +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.fields.AutoSizeTextField +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.common.toUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account +import kotlinx.collections.immutable.toImmutableList + +@Suppress("LongMethod", "MagicNumber") +@Composable +internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = state.title.resolveReference(), + onBackClick = state.onCloseClick, + iconRes = R.drawable.ic_close_24, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f), + + ) { + AccountSummary(state.account) + SpacerH24() + AccountColor(state.colorsState) + SpacerH24() + AccountIcon(state.iconsState) + SpacerH8() + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = state.account.derivationInfo.text.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + enabled = state.buttonState.isButtonEnabled, + text = state.buttonState.text.resolveReference(), + onClick = state.buttonState.onConfirmClick, + ) + } +} + +@Composable +private fun AccountSummary(account: Account) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + AccountIcon(account) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResourceSafe(R.string.account_form_name), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.height(2.dp)) + + AutoSizeTextField( + centered = true, + textStyle = TangemTheme.typography.head, + placeholder = account.inputPlaceholder, + value = account.name, + singleLine = true, + onValueChange = account.onNameChange, + ) + SpacerH(20.dp) + } +} + +@Composable +private fun AccountIcon(account: Account) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(88.dp) + .clip(RoundedCornerShape(TangemTheme.dimens.radius24)) + .background(account.portfolioIcon.color.getUiColor()), + ) { + val icon = account.portfolioIcon.value + val letter = account.name.firstOrNull() + ?: account.inputPlaceholder.resolveReference().first() + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = letter.uppercase(), + style = TangemTheme.typography.head, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = Modifier.size(44.dp), + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { + Box( + Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + ) { + val columns = GridCells.Fixed(6) + val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp) + LazyVerticalGrid( + columns = columns, + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + itemsIndexed(colorsState.list) { index, color -> + val isSelected = color == colorsState.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .wrapContentSize() + .clickable(onClick = { colorsState.onColorSelect(color) }) + .size(48.dp), + ) { + if (isSelected) { + Box( + modifier = Modifier + .size(47.dp) + .border(2.dp, color.getUiColor(), shape = CircleShape), + ) + Box( + modifier = Modifier + .size(36.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } else { + Box( + modifier = Modifier + .size(40.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } + } + } + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) { + Box( + Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action) + .padding(8.dp), + ) { + val columns = GridCells.Fixed(6) + LazyVerticalGrid( + columns = columns, + ) { + itemsIndexed(iconsState.list) { index, icon -> + val isSelected = icon == iconsState.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .wrapContentSize() + .clickable(onClick = { iconsState.onIconSelect(icon) }) + .size(52.dp), + ) { + if (isSelected) { + val borderColor: Color + val iconTint: Color + val backgroundTint: Color + if (index == 0) { + borderColor = TangemTheme.colors.icon.accent + iconTint = TangemTheme.colors.icon.accent + backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + } else { + borderColor = TangemTheme.colors.icon.informative + iconTint = TangemTheme.colors.icon.secondary + backgroundTint = TangemTheme.colors.field.focused + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(44.dp) + .border(2.dp, borderColor, shape = CircleShape), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .background(color = backgroundTint, shape = CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + tint = iconTint, + ) + } + } + } else { + val iconTint: Color + val backgroundTint: Color + if (index == 0) { + iconTint = TangemTheme.colors.icon.accent + backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + } else { + iconTint = TangemTheme.colors.text.tertiary + backgroundTint = TangemTheme.colors.field.focused + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .background(color = backgroundTint, shape = CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + tint = iconTint, + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountCreateEditUM) { + TangemThemePreview { + AccountCreateEditContent(state = params) + } +} + +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + val colors = CryptoPortfolioIcon.Color.entries.toImmutableList() + val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList() + var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + val first = AccountCreateEditUM( + title = stringReference("Add account"), + onCloseClick = {}, + account = Account( + name = "", + portfolioIcon = portfolioIcon, + inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account), + onNameChange = {}, + derivationInfo = AccountCreateEditUM.DerivationInfo.Content( + text = resourceReference(id = R.string.account_form_account_index, formatArgs = wrappedList(1)), + index = 1, + ), + ), + colorsState = AccountCreateEditUM.Colors( + selected = portfolioIcon.color, + onColorSelect = {}, + list = colors.toImmutableList(), + ), + iconsState = AccountCreateEditUM.Icons( + selected = portfolioIcon.value, + onIconSelect = {}, + list = icons.toImmutableList(), + ), + buttonState = AccountCreateEditUM.Button( + isButtonEnabled = false, + onConfirmClick = {}, + text = stringReference("Add account"), + ), + ) + add(first) + + portfolioIcon = portfolioIcon.copy( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.entries.random(), + ) + val second = AccountCreateEditUM( + title = stringReference("Edit account"), + onCloseClick = {}, + account = Account( + portfolioIcon = portfolioIcon, + name = "Main account", + inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), + onNameChange = {}, + derivationInfo = AccountCreateEditUM.DerivationInfo.Content( + text = resourceReference(id = R.string.account_form_account_index, formatArgs = wrappedList(1)), + index = 1, + ), + ), + colorsState = AccountCreateEditUM.Colors( + selected = portfolioIcon.color, + onColorSelect = {}, + list = colors.toImmutableList(), + ), + iconsState = AccountCreateEditUM.Icons( + selected = portfolioIcon.value, + onIconSelect = {}, + list = icons.toImmutableList(), + ), + buttonState = AccountCreateEditUM.Button( + isButtonEnabled = false, + onConfirmClick = {}, + text = stringReference("Save"), + ), + ) + add(second) + }, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt new file mode 100644 index 0000000000..2ba7a2febd --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -0,0 +1,85 @@ +package com.tangem.features.account.details + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase +import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.common.toUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon +import com.tangem.features.account.details.entity.AccountDetailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class AccountDetailsModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow get() = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun onEditAccountClick() { + router.push(AppRoute.EditAccount(params.account)) + } + + private fun onManageTokensClick() { + // todo account add account param + router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS)) + } + + private fun onArchiveAccountClick() { + confirmArchiveDialog() + } + + private fun confirmArchiveDialog() { + val secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_details_archive_action), + warning = true, + onClick = ::archiveCryptoPortfolio, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_details_archive), + message = resourceReference(R.string.account_details_archive_description), + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun archiveCryptoPortfolio() = modelScope.launch { + archiveCryptoPortfolioUseCase(params.account.accountId) + } + + private fun getInitialState(): AccountDetailsUM { + return AccountDetailsUM( + accountName = params.account.name.value, + accountIcon = params.account.portfolioIcon.toUM(), + onCloseClick = { router.pop() }, + onAccountEditClick = ::onEditAccountClick, + onManageTokensClick = ::onManageTokensClick, + onArchiveAccountClick = ::onArchiveAccountClick, + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt new file mode 100644 index 0000000000..5051ca3134 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.details + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.details.ui.AccountDetailsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAccountDetailsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: AccountDetailsComponent.Params, +) : AppComponentContext by appComponentContext, AccountDetailsComponent { + + private val model: AccountDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AccountDetailsContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : AccountDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: AccountDetailsComponent.Params, + ): DefaultAccountDetailsComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt new file mode 100644 index 0000000000..fdc4feda8a --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.details.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.details.AccountDetailsModel +import com.tangem.features.account.details.DefaultAccountDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountDetailsModule { + + @Binds + fun bindAccountDetailsComponentFactory( + impl: DefaultAccountDetailsComponent.Factory, + ): AccountDetailsComponent.Factory + + @Binds + @IntoMap + @ClassKey(AccountDetailsModel::class) + fun bindAccountDetailsModel(model: AccountDetailsModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt new file mode 100644 index 0000000000..7b2bc9b44d --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.account.details.entity + +import com.tangem.features.account.common.CryptoPortfolioIconUM + +data class AccountDetailsUM( + val accountName: String, + val accountIcon: CryptoPortfolioIconUM, + val onCloseClick: () -> Unit, + val onAccountEditClick: () -> Unit, + val onManageTokensClick: () -> Unit, + val onArchiveAccountClick: () -> Unit, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt new file mode 100644 index 0000000000..08b3562176 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -0,0 +1,236 @@ +package com.tangem.features.account.details.ui + +import android.content.res.Configuration +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.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.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.account.getResId +import com.tangem.common.ui.account.getUiColor +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.AutoSizeTextField +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.common.CryptoPortfolioIconUM +import com.tangem.features.account.common.toUM +import com.tangem.features.account.details.entity.AccountDetailsUM + +@Composable +internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + onBackClick = state.onCloseClick, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16) + .weight(1f), + + ) { + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + text = stringResourceSafe(R.string.account_details_title), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + SpacerH16() + AccountRow(state) + SpacerH16() + ManageTokensRow(state) + SpacerH16() + ArchiveAccountRow(state) + SpacerH(8.dp) + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.account_details_archive_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Composable +private fun ArchiveAccountRow(state: AccountDetailsUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onArchiveAccountClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + tint = TangemTheme.colors.icon.warning, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24), + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.account_details_archive), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.subtitle1, + ) + } +} + +@Composable +private fun ManageTokensRow(state: AccountDetailsUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onManageTokensClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_group_24), + tint = TangemTheme.colors.icon.secondary, + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.main_manage_tokens), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun AccountRow(state: AccountDetailsUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onAccountEditClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)), + accountName = state.accountName, + accountIcon = state.accountIcon, + ) + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = stringResourceSafe(R.string.account_form_name), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + AutoSizeTextField( + textStyle = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + value = state.accountName, + singleLine = true, + readOnly = true, + onValueChange = {}, + ) + } + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.common_edit), + onClick = state.onAccountEditClick, + ), + ) + } +} + +// todo account make reusable +@Composable +internal fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.background(accountIcon.color.getUiColor()), + ) { + val icon = accountIcon.value + val letter = accountName.first() + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = letter.uppercase(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = Modifier.size(20.dp), + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountDetailsUM) { + TangemThemePreview { + AccountDetailsContent(state = params) + } +} + +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + val first = AccountDetailsUM( + onCloseClick = {}, + onAccountEditClick = {}, + onManageTokensClick = {}, + onArchiveAccountClick = {}, + accountName = "Main", + accountIcon = portfolioIcon, + ) + add(first) + portfolioIcon = portfolioIcon.copy( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.entries.random(), + ) + add(first.copy(accountIcon = portfolioIcon)) + }, +) \ No newline at end of file diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index 4ee4b4358b..def97bf1f3 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { api(projects.features.biometry.api) + implementation(projects.features.hotWallet.api) /** Core modules */ implementation(projects.core.ui) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 933716b04b..614b462e24 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -13,13 +13,15 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -40,11 +42,13 @@ internal class AskBiometryModel @Inject constructor( private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, private val settingsRepository: SettingsRepository, private val tangemSdkManager: TangemSdkManager, - private val userWalletsListManager: UserWalletsListManager, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val walletsRepository: WalletsRepository, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsManager: SettingsManager, private val uiMessageSender: UiMessageSender, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -87,7 +91,7 @@ internal class AskBiometryModel @Inject constructor( * because it will be automatically saved on UserWalletsListManager switch */ - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync ?: run { + val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run { Timber.e("Unable to save user wallet") uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), @@ -109,10 +113,18 @@ internal class AskBiometryModel @Inject constructor( walletsRepository.saveShouldSaveUserWallets(item = true) settingsRepository.setShouldSaveAccessCodes(value = true) - if (userWallet is UserWallet.Cold) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + walletsRepository.setUseBiometricAuthentication(value = true) + setBiometryLockForAllWallets() cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, + isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(), ) + } else { + if (userWallet is UserWallet.Cold) { + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) + } } if (_uiState.value.bottomSheetVariant) { @@ -123,6 +135,18 @@ internal class AskBiometryModel @Inject constructor( params.modelCallbacks.onAllowed() } + private fun setBiometryLockForAllWallets() { + modelScope.launch { + userWalletsListRepository.userWalletsSync().forEach { userWallet -> + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + } + private fun showEnrollBiometricsDialog() { uiMessageSender.send( DialogMessage( diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 828a0ccfac..da58579502 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -18,6 +18,12 @@ dependencies { /** Hot Wallet Feature */ implementation(projects.features.hotWallet.api) + /** Project - Domain */ + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.wallets) + implementation(projects.domain.models) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.analytics) @@ -45,7 +51,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 242046b568..23d94a6c78 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -1,19 +1,63 @@ package com.tangem.features.createwalletselection +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -31,10 +75,110 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onHardwareWalletClick() { - // TODO open card order web page + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onScanClick() { - // TODO open card scanning + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) } } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 0c777f9a97..4106c3d8c6 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,10 +5,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -18,6 +20,7 @@ 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.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -179,6 +182,9 @@ private fun AlreadyHaveTangemWalletBlock( isScanInProgress: Boolean, modifier: Modifier = Modifier, ) { + var buttonWidth by remember { mutableStateOf(0) } + val density = LocalDensity.current + Row( modifier = modifier .fillMaxWidth() @@ -201,9 +207,17 @@ private fun AlreadyHaveTangemWalletBlock( style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, ) + TangemButton( modifier = Modifier - .wrapContentWidth(), + .conditional(buttonWidth > 0) { + width(with(density) { buttonWidth.toDp() }) + } + .onGloballyPositioned { coordinates -> + if (buttonWidth == 0) { + buttonWidth = coordinates.size.width + } + }, text = stringResourceSafe(R.string.wallet_create_scan_title), onClick = onScanClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index a5b029fa67..e02206355e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -118,10 +118,14 @@ internal class DetailsModel @Inject constructor( modelScope.launch { val userWallets = getWalletsUseCase.invokeSync() - val scanResponse = - getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY] - ?: error("Selected wallet is null") + val selectedUserWallet = getSelectedWalletSyncUseCase().getOrNull() + ?: error("Selected wallet is null") + if (selectedUserWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Send feedback + } + + val scanResponse = selectedUserWallet.requireColdWallet().scanResponse val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch val feedbackType = when { @@ -140,11 +144,13 @@ internal class DetailsModel @Inject constructor( } private fun openUseDesk() { - val scanResponse = - getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY] - ?: error("Selected wallet is null") + val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null") - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] UseDesk + } + + val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return router.push(AppRoute.Usedesk(cardInfo)) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 6af5b54fe3..d1d008e54f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -36,6 +36,7 @@ internal class UserWalletListModel @Inject constructor( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = false, + authMode = false, onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 10c6667755..7915a2a320 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -21,7 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index 3454262136..b34ba43590 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.compose.accompanist.permission) implementation(deps.compose.accompanist.webView) implementation(deps.compose.material3) - implementation(deps.compose.material) /** Core modules */ implementation(projects.core.ui) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index a34f346733..50fe65280b 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -57,7 +57,7 @@ internal class DisclaimerModel @Inject constructor( } else { neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION) - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Home()) } } } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 1ae99e8068..1a01b428fb 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import com.google.accompanist.web.WebView import com.google.accompanist.web.WebViewNavigator @@ -62,9 +64,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { ) { TangemTopAppBar( title = resourceReference(R.string.disclaimer_title), - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.popBack, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.popBack, ).takeIf { state.isTosAccepted }, titleAlignment = Alignment.CenterHorizontally, textColor = textColor, @@ -108,7 +109,11 @@ private fun DisclaimerContent(url: String) { state = webViewState, modifier = Modifier .fillMaxSize() - .background(TangemTheme.colors.background.primary), + .background(TangemTheme.colors.background.primary) + .testTag(DisclaimerScreenTestTags.WEB_VIEW) + .semantics { + contentDescription = "WebView URL: ${webViewState.content.getCurrentUrl() ?: url}" + }, captureBackPresses = false, navigator = webViewNavigator, onCreated = WebView::applySafeSettings, diff --git a/features/home/api/.gitignore b/features/home/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/home/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/home/api/build.gradle.kts b/features/home/api/build.gradle.kts new file mode 100644 index 0000000000..7f07d748d7 --- /dev/null +++ b/features/home/api/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.home.api" +} + +dependencies { + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Common */ + implementation(projects.common.routing) +} \ No newline at end of file diff --git a/features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt b/features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt new file mode 100644 index 0000000000..05a94c2090 --- /dev/null +++ b/features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.home.api + +import com.tangem.common.routing.entity.InitScreenLaunchMode +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface HomeComponent : ComposableContentComponent { + + data class Params( + val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, + ) + + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Params): HomeComponent + } +} \ No newline at end of file diff --git a/features/home/impl/.gitignore b/features/home/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/home/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts new file mode 100644 index 0000000000..d06bedde20 --- /dev/null +++ b/features/home/impl/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.home.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.home.api) + implementation(projects.features.hotWallet.api) + + /** Core modules */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.navigation) + + /** Common */ + implementation(projects.common.routing) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.core) + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.legacy) + implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) + + /** AndroidX libraries */ + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + + /** Tangem libraries */ + implementation(tangemDeps.card.android) + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) + + /** Other libraries */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt new file mode 100644 index 0000000000..7c210b9af7 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.home.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.home.api.HomeComponent +import com.tangem.features.home.impl.model.HomeModel +import com.tangem.features.home.impl.ui.Home +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultHomeComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: HomeComponent.Params, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, +) : HomeComponent, AppComponentContext by appComponentContext { + + private val model: HomeModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + Home( + state = state, + modifier = modifier, + isV2StoriesEnabled = hotWalletFeatureToggles.isHotWalletEnabled, + ) + } + + @AssistedFactory + interface Factory : HomeComponent.Factory { + override fun create(context: AppComponentContext, params: HomeComponent.Params): DefaultHomeComponent + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt new file mode 100644 index 0000000000..cb6516bc9b --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.home.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.home.api.HomeComponent +import com.tangem.features.home.impl.DefaultHomeComponent +import com.tangem.features.home.impl.model.HomeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultHomeComponent.Factory): HomeComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(HomeModel::class) + fun provideModel(model: HomeModel): Model +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt new file mode 100644 index 0000000000..70636eda60 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -0,0 +1,251 @@ +package com.tangem.features.home.impl.model + +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRoute.ManageTokens.Source +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.entity.InitScreenLaunchMode +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.home.api.HomeComponent +import com.tangem.features.home.impl.ui.state.HomeUM +import com.tangem.features.home.impl.ui.state.Stories +import com.tangem.features.home.impl.ui.state.getRestrictedStories +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject + +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") +@ModelScoped +internal class HomeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val router: Router, + private val appRouter: AppRouter, + private val getUserCountryUseCase: GetUserCountryUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, +) : Model() { + + val params = paramsContainer.require() + + private val _uiState = MutableStateFlow( + HomeUM( + scanInProgress = false, + stories = getRestrictedStories().toImmutableList(), + onScanClick = ::onScanClick, + onShopClick = ::onShopClick, + onSearchTokensClick = ::onSearchTokensClick, + onCreateNewWalletClick = ::onCreateNewWalletClick, + onAddExistingWalletClick = ::onAddExistingWalletClick, + ), + ) + + val uiState = _uiState.asStateFlow() + + init { + analyticsEventHandler.send(IntroductionProcess.ScreenOpened) + observeUserCountryChanges() + + when (params.launchMode) { + InitScreenLaunchMode.Standard -> Unit + InitScreenLaunchMode.WithCardScan -> scanCard() + } + } + + private fun observeUserCountryChanges() { + getUserCountryUseCase.invoke() + .distinctUntilChanged() + .filterNotNull() + .onEach { result -> + val userCountry = result.getOrNull() ?: UserCountry.Other(Locale.getDefault().country) + updateStoriesForCountry(userCountry) + } + .flowOn(dispatchers.io) + .launchIn(modelScope) + } + + private fun updateStoriesForCountry(userCountry: UserCountry) { + val stories = if (userCountry.needApplyFCARestrictions()) { + getRestrictedStories() + } else { + Stories.entries + } + + _uiState.update { + it.copy(stories = stories.toImmutableList()) + } + } + + private fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onSearchTokensClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonTokensList) + router.push(AppRoute.ManageTokens(Source.STORIES)) + } + + private fun onCreateNewWalletClick() { + router.push(AppRoute.CreateWalletSelection) + } + + private fun onAddExistingWalletClick() { + router.push(AppRoute.AddExistingWallet) + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + _uiState.update { it.copy(scanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt new file mode 100644 index 0000000000..24025e1627 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt @@ -0,0 +1,35 @@ +package com.tangem.features.home.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.SystemBarsIconsDisposable +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect +import com.tangem.features.home.impl.ui.compose.StoriesScreen +import com.tangem.features.home.impl.ui.compose.StoriesScreenV2 +import com.tangem.features.home.impl.ui.state.HomeUM + +@Composable +internal fun Home(state: HomeUM, isV2StoriesEnabled: Boolean, modifier: Modifier = Modifier) { + SystemBarsIconsDisposable(darkIcons = false) + + if (isV2StoriesEnabled) { + StoriesScreenV2( + modifier = modifier, + state = state, + onCreateNewWalletButtonClick = state.onCreateNewWalletClick, + onAddExistingWalletButtonClick = state.onAddExistingWalletClick, + onScanButtonClick = state.onScanClick, + ) + } else { + StoriesScreen( + modifier = modifier, + state = state, + onScanButtonClick = state.onScanClick, + onShopButtonClick = state.onShopClick, + onSearchTokensClick = state.onSearchTokensClick, + ) + } + + ChangeRootBackgroundColorEffect(TangemColorPalette.Black) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesAnimation.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesAnimation.kt index 5265c91db4..3bf071d96a 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesAnimation.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose +package com.tangem.features.home.impl.ui.compose import androidx.compose.animation.core.* import androidx.compose.foundation.Image @@ -19,8 +19,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.tangem.tap.common.compose.extensions.AnimatedValue -import com.tangem.tap.common.compose.extensions.toAnimatable +import com.tangem.core.ui.utils.AnimatedValue +import com.tangem.core.ui.utils.toAnimatable private const val SCALE_SWITCH_BARRIER = 1.15f diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt index 3ccd4ba7fe..704485cb28 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("MagicNumber") -package com.tangem.tap.features.home.compose +package com.tangem.features.home.impl.ui.compose import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -23,24 +23,23 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.tap.features.home.compose.content.* -import com.tangem.tap.features.home.compose.views.HomeButtons -import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton -import com.tangem.tap.features.home.compose.views.StoriesProgressBar -import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.home.redux.Stories -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.content.* +import com.tangem.features.home.impl.ui.compose.views.HomeButtons +import com.tangem.features.home.impl.ui.compose.views.SearchCurrenciesButton +import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar +import com.tangem.features.home.impl.ui.state.Stories +import com.tangem.core.ui.R +import com.tangem.features.home.impl.ui.state.HomeUM import kotlin.math.max @Composable internal fun StoriesScreen( - homeState: MutableState, + state: HomeUM, onScanButtonClick: () -> Unit, onShopButtonClick: () -> Unit, onSearchTokensClick: () -> Unit, + modifier: Modifier = Modifier, ) { - val state = homeState.value - var currentStory by remember { mutableStateOf(state.firstStory) } val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) @@ -59,14 +58,14 @@ internal fun StoriesScreen( // todo refactor [REDACTED_TASK_KEY] StoriesScreenContent( - modifier = Modifier + modifier = modifier .fillMaxSize() .testTag(StoriesScreenTestTags.SCREEN_CONTAINER), config = StoriesScreenContentConfig( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, currentStory = currentStory, - isScanInProgress = homeState.value.scanInProgress, + isScanInProgress = state.scanInProgress, onGoToPreviousStory = goToPreviousStory, onGoToNextStory = goToNextStory, onSearchTokensClick = onSearchTokensClick, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt index 5ba5f44315..0554472199 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt @@ -1,6 +1,6 @@ @file:Suppress("MagicNumber") -package com.tangem.tap.features.home.compose +package com.tangem.features.home.impl.ui.compose import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -20,23 +20,22 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.tap.features.home.compose.content.* -import com.tangem.tap.features.home.compose.views.HomeButtonsV2 -import com.tangem.tap.features.home.compose.views.StoriesProgressBar -import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.home.redux.Stories -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.content.* +import com.tangem.features.home.impl.ui.compose.views.HomeButtonsV2 +import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar +import com.tangem.features.home.impl.ui.state.Stories import kotlin.math.max +import com.tangem.core.ui.R +import com.tangem.features.home.impl.ui.state.HomeUM @Composable internal fun StoriesScreenV2( - homeState: MutableState, + state: HomeUM, onCreateNewWalletButtonClick: () -> Unit, onAddExistingWalletButtonClick: () -> Unit, onScanButtonClick: () -> Unit, + modifier: Modifier = Modifier, ) { - val state = homeState.value - var currentStory by remember { mutableStateOf(state.firstStory) } val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) @@ -55,14 +54,14 @@ internal fun StoriesScreenV2( // todo refactor [REDACTED_TASK_KEY] StoriesScreenContentV2( - modifier = Modifier + modifier = modifier .fillMaxSize() .testTag(StoriesScreenTestTags.SCREEN_CONTAINER), config = StoriesScreenContentV2Config( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, currentStory = currentStory, - isScanInProgress = homeState.value.scanInProgress, + isScanInProgress = state.scanInProgress, onGoToPreviousStory = goToPreviousStory, onGoToNextStory = goToNextStory, onCreateNewWalletButtonClick = onCreateNewWalletButtonClick, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/Content.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/Content.kt index 1635f234ad..33bf6db728 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/Content.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -21,9 +21,9 @@ import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation -import com.tangem.tap.features.home.compose.StoriesTextAnimation -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.StoriesBottomImageAnimation +import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation +import com.tangem.core.ui.R @Composable fun StoriesRevolutionaryWallet() { diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/CurrenciesWeb3Content.kt similarity index 93% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/CurrenciesWeb3Content.kt index ec1cb88957..66f639bc51 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/CurrenciesWeb3Content.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -17,12 +17,10 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.extensions.dpSize -import com.tangem.tap.common.compose.extensions.halfHeight -import com.tangem.tap.common.compose.extensions.toPx -import com.tangem.tap.common.extensions.isEven -import com.tangem.tap.features.home.compose.HorizontalSlidingImage -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.HorizontalSlidingImage +import com.tangem.core.ui.R +import com.tangem.core.ui.utils.dpSize +import com.tangem.core.ui.utils.toPx @Composable fun StoriesCurrenciesContent(paused: Boolean, duration: Int) { @@ -138,4 +136,8 @@ private val BottomGradient: Brush = Brush.verticalGradient( TangemColorPalette.Black.copy(alpha = 0.95f), TangemColorPalette.Black, ), -) \ No newline at end of file +) + +fun DpSize.halfHeight(): Dp = this.height / 2 + +fun Int.isEven() = this and 1 == 0 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FirstStoriesContent.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FirstStoriesContent.kt index 42fdcaf8fa..f7f6debd9c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FirstStoriesContent.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing @@ -25,8 +25,8 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.home.compose.StoriesTextAnimation -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation +import com.tangem.core.ui.R @Suppress("LongMethod", "ComplexMethod", "MagicNumber") @Composable diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FloatingCardsContent.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FloatingCardsContent.kt index 6a010d28ed..e846781ec0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FloatingCardsContent.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* @@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.graphicsLayer -import com.tangem.tap.common.compose.extensions.AnimatedValue -import com.tangem.tap.common.compose.extensions.asImageBitmap -import com.tangem.tap.common.compose.extensions.toAnimatable -import com.tangem.wallet.R +import com.tangem.core.ui.R +import com.tangem.core.ui.utils.AnimatedValue +import com.tangem.core.ui.utils.asImageBitmap +import com.tangem.core.ui.utils.toAnimatable /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtons.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtons.kt index eab6ba4ced..5b0615495b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtons.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.wallet.R +import com.tangem.core.ui.R @Composable internal fun HomeButtons( diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtonsV2.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtonsV2.kt index dc2192b603..c68825891d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtonsV2.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -19,7 +19,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.wallet.R +import com.tangem.core.ui.R @Composable internal fun HomeButtonsV2( diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/SearchCurrenciesButton.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/SearchCurrenciesButton.kt index 4d70d5f2f6..0987e2193b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/SearchCurrenciesButton.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -12,7 +12,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.wallet.R +import com.tangem.core.ui.R @Composable internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt index 66fc2bd6f1..ddcc652d3f 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.material3.ButtonColors import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesProgressBar.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesProgressBar.kt index ef3514bce4..cc6ea2107c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesProgressBar.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import android.provider.Settings import androidx.compose.animation.core.Animatable diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt index b1d45779ba..d727df8990 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt @@ -1,15 +1,16 @@ -package com.tangem.tap.features.home.redux +package com.tangem.features.home.impl.ui.state import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import org.rekotlin.StateType - -// todo refactor [REDACTED_TASK_KEY] -data class HomeState( - val scanInProgress: Boolean = false, - val stories: ImmutableList = getRestrictedStories().toImmutableList(), -) : StateType { +data class HomeUM( + val scanInProgress: Boolean, + val stories: ImmutableList, + val onScanClick: () -> Unit, + val onShopClick: () -> Unit, + val onSearchTokensClick: () -> Unit, + val onCreateNewWalletClick: () -> Unit, + val onAddExistingWalletClick: () -> Unit, +) { val firstStory: Stories get() = stories[0] fun stepOf(story: Stories): Int = stories.indexOf(story) diff --git a/features/hot-wallet/api/build.gradle.kts b/features/hot-wallet/api/build.gradle.kts index 310329cc67..a51cd40254 100644 --- a/features/hot-wallet/api/build.gradle.kts +++ b/features/hot-wallet/api/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) /* Project - Core */ diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt new file mode 100644 index 0000000000..84d8828d8f --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface CreateWalletBackupComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt index 6e6c9c7c8b..49b893e3a6 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.hotwallet import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester interface HotAccessCodeRequestComponent : ComposableContentComponent, HotWalletPasswordRequester { diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt deleted file mode 100644 index eac354ece4..0000000000 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.hotwallet - -import com.tangem.hot.sdk.model.HotAuth - -interface HotWalletPasswordRequester { - - suspend fun wrongPassword() - - suspend fun requestPassword(hasBiometry: Boolean): Result - - suspend fun dismiss() - - sealed class Result { - data object UseBiometry : Result() - data object Dismiss : Result() - data class EnteredPassword(val password: HotAuth.Password) : Result() - } -} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt new file mode 100644 index 0000000000..b2416c8d0a --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt @@ -0,0 +1,10 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface UpdateAccessCodeComponent : ComposableContentComponent { + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt new file mode 100644 index 0000000000..8b99c19f36 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface WalletActivationComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index c740fecd23..2f25fc3478 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.datasource) /** Domain */ + implementation(projects.domain.card) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) @@ -52,7 +53,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt similarity index 57% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index b1f774aa93..5a044f85ef 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.setaccesscode +package com.tangem.features.hotwallet.accesscode import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,36 +8,45 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect -import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent +import com.tangem.features.hotwallet.accesscode.ui.AccessCode +import com.tangem.domain.models.wallet.UserWalletId import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class SetAccessCodeComponent @AssistedInject constructor( +internal class AccessCodeComponent @AssistedInject constructor( @Assisted private val context: AppComponentContext, @Assisted private val params: Params, ) : ComposableContentComponent, AppComponentContext by context { - private val model: SetAccessCodeModel = getOrCreateModel(params) + private val model: AccessCodeModel = getOrCreateModel(params) @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - SetAccessCodeContent( - state = state, - onBack = { model.onBack() }, - modifier = modifier, - ) - DisableScreenshotsDisposableEffect() + + AccessCode( + modifier = modifier, + state = state, + ) } interface ModelCallbacks { - fun onBackClick() - fun onAccessCodeSet() + fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) + fun onAccessCodeConfirmed(userWalletId: UserWalletId) } data class Params( + val isConfirmMode: Boolean, + val accessCodeToConfirm: String? = null, + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) + + @AssistedFactory + interface Factory { + fun create(context: AppComponentContext, params: Params): AccessCodeComponent + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt new file mode 100644 index 0000000000..2db69f48ee --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -0,0 +1,130 @@ +package com.tangem.features.hotwallet.accesscode + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.UnlockHotWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@Stable +@ModelScoped +internal class AccessCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, + private val tangemHotSdk: TangemHotSdk, +) : Model() { + + private val params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + private fun getInitialState() = AccessCodeUM( + accessCode = "", + onAccessCodeChange = ::onAccessCodeChange, + isConfirmMode = params.isConfirmMode, + buttonEnabled = false, + buttonInProgress = false, + onButtonClick = ::onButtonClick, + ) + + private fun onAccessCodeChange(value: String) { + uiState.update { + it.copy( + accessCode = value, + buttonEnabled = if (params.isConfirmMode) { + value == params.accessCodeToConfirm + } else { + value.length == uiState.value.accessCodeLength + }, + ) + } + } + + private fun onButtonClick() { + if (!params.isConfirmMode) { + params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode) + } else { + params.accessCodeToConfirm?.let { + setCode(params.userWalletId, it) + } + } + } + + private fun setCode(userWalletId: UserWalletId, accessCode: String) { + modelScope.launch { + uiState.update { + it.copy(buttonInProgress = true) + } + + runCatching { + val userWallet = getUserWalletUseCase(userWalletId) + .getOrElse { error("User wallet with id $userWalletId not found") } + if (userWallet !is UserWallet.Hot) return@launch + + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = updatedHotWalletId, + auth = HotAuth.Password(accessCode.toCharArray()), + ), + auth = HotAuth.Biometry, + ) + } + + userWalletsListRepository.saveWithoutLock( + userWallet.copy( + hotWalletId = updatedHotWalletId, + backedUp = true, + ), + canOverride = true, + ) + + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), + ) + + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } + + params.callbacks.onAccessCodeConfirmed(params.userWalletId) + }.onFailure { + Timber.e(it) + + uiState.update { + it.copy(buttonInProgress = false) + } + } + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt new file mode 100644 index 0000000000..ca5ecaa367 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt @@ -0,0 +1,3 @@ +package com.tangem.features.hotwallet.accesscode + +const val ACCESS_CODE_LENGTH = 6 \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/SetAccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt similarity index 53% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/SetAccessCodeModule.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt index fccaaac528..57353dd676 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/SetAccessCodeModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt @@ -1,7 +1,7 @@ -package com.tangem.features.hotwallet.setaccesscode.di +package com.tangem.features.hotwallet.accesscode.di import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel +import com.tangem.features.hotwallet.accesscode.AccessCodeModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -11,10 +11,10 @@ import dagger.multibindings.IntoMap @Module @InstallIn(SingletonComponent::class) -internal interface SetAccessCodeModule { +internal interface AccessCodeModule { @Binds @IntoMap - @ClassKey(SetAccessCodeModel::class) - fun bindSetAccessCodeModel(model: SetAccessCodeModel): Model + @ClassKey(AccessCodeModel::class) + fun bindAccessCodeModel(model: AccessCodeModel): Model } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt new file mode 100644 index 0000000000..11eccc1463 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet.accesscode.entity + +import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH + +internal data class AccessCodeUM( + val accessCode: String, + val onAccessCodeChange: (String) -> Unit, + val isConfirmMode: Boolean, + val buttonEnabled: Boolean, + val buttonInProgress: Boolean, + val onButtonClick: () -> Unit, +) { + val accessCodeLength: Int = ACCESS_CODE_LENGTH +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt new file mode 100644 index 0000000000..3924d978c7 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -0,0 +1,138 @@ +package com.tangem.features.hotwallet.accesscode.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM + +@Suppress("LongParameterList", "LongMethod") +@Composable +internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Column( + modifier = Modifier + .padding(top = 16.dp) + .weight(1f) + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier + .padding(top = 56.dp) + .align(Alignment.CenterHorizontally), + text = if (state.isConfirmMode) { + stringResourceSafe(R.string.access_code_confirm_title) + } else { + stringResourceSafe(R.string.access_code_create_title) + }, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterHorizontally), + text = if (state.isConfirmMode) { + stringResourceSafe(R.string.access_code_confirm_description) + } else { + stringResourceSafe( + R.string.access_code_create_description, + state.accessCodeLength, + ) + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + contentAlignment = Alignment.Center, + ) { + PinTextField( + length = state.accessCodeLength, + isPasswordVisual = !state.isConfirmMode, + value = state.accessCode, + pinTextColor = PinTextColor.Primary, + onValueChange = state.onAccessCodeChange, + ) + } + } + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + text = stringResourceSafe( + if (state.isConfirmMode) { + R.string.common_confirm + } else { + R.string.common_continue + }, + ), + onClick = state.onButtonClick, + enabled = state.buttonEnabled, + showProgress = state.buttonInProgress, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewSet() { + TangemThemePreview { + AccessCode( + state = AccessCodeUM( + accessCode = "", + onAccessCodeChange = {}, + isConfirmMode = false, + buttonEnabled = false, + buttonInProgress = false, + onButtonClick = {}, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewConfirm() { + TangemThemePreview { + AccessCode( + state = AccessCodeUM( + accessCode = "123456", + onAccessCodeChange = {}, + isConfirmMode = true, + buttonEnabled = true, + buttonInProgress = false, + onButtonClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index beaba4f29a..4dd7d01895 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -1,14 +1,13 @@ package com.tangem.features.hotwallet.accesscoderequest -import androidx.compose.foundation.focusable import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.FullScreen +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.HotAccessCodeRequestComponent -import com.tangem.features.hotwallet.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,8 +25,14 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( model.wrongAccessCode() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { - model.show(hasBiometry) + override suspend fun successfulAuthentication() { + model.successfulAuthentication() + } + + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result { + model.show(attemptRequest) return model.waitResult() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 28fb18f009..4a75ad8438 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -2,36 +2,64 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.HotWalletPasswordRequester +import com.tangem.core.ui.components.fields.PinTextColor +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.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM +import com.tangem.features.hotwallet.impl.R import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { private val result = MutableStateFlow(null) + private val currentRequest = MutableStateFlow(null) + private val attemptsRequestJobHolder = JobHolder() + + private val HotWalletPasswordRequester.AttemptRequest.attemptId + get() = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = authMode, + ) val uiState: StateFlow field = MutableStateFlow(getInitialState()) - fun dismiss() { - result.value = HotWalletPasswordRequester.Result.Dismiss - dismissState() - } + suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) { + if (userWalletExists(attemptRequest.hotWalletId).not()) { + Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") + result.value = HotWalletPasswordRequester.Result.Dismiss + return + } - fun show(hasBiometry: Boolean) { + currentRequest.value = attemptRequest result.value = null // Reset the result when showing the dialog + subscribeToAttempts(id = attemptRequest.attemptId) uiState.update { it.copy( isShown = true, accessCode = "", - useBiometricVisible = hasBiometry, + useBiometricVisible = attemptRequest.hasBiometry, onAccessCodeChange = ::onAccessCodeChange, ) } @@ -41,16 +69,36 @@ internal class HotAccessCodeRequestModel @Inject constructor( return result.filterNotNull().first().also { result.value = null } } + fun dismiss() { + result.value = HotWalletPasswordRequester.Result.Dismiss + attemptsRequestJobHolder.cancel() + dismissState() + } + suspend fun wrongAccessCode() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.incrementAttempts(currentRequest.attemptId) uiState.update { it.copy( - wrongAccessCode = true, + accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, ) } delay(timeMillis = 500) // Delay to show the wrong access code state } + suspend fun successfulAuthentication() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.resetAttempts(currentRequest.hotWalletId) + uiState.update { + it.copy( + accessCodeColor = PinTextColor.Success, + onAccessCodeChange = {}, + ) + } + delay(timeMillis = 200) // Delay to show the success state + } + private fun getInitialState() = HotAccessCodeRequestUM( onDismiss = ::dismiss, onAccessCodeChange = ::onAccessCodeChange, @@ -65,7 +113,10 @@ internal class HotAccessCodeRequestModel @Inject constructor( if (accessCode.length > ACCESS_CODE_LENGTH) return uiState.update { - it.copy(accessCode = accessCode, wrongAccessCode = false) + it.copy( + accessCode = accessCode, + accessCodeColor = PinTextColor.Primary, + ) } if (accessCode.length == ACCESS_CODE_LENGTH) { @@ -77,13 +128,71 @@ internal class HotAccessCodeRequestModel @Inject constructor( } } + private fun subscribeToAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + fun remainingSecondsToText(remainingSeconds: Int): TextReference? { + return if (remainingSeconds > 0) { + resourceReference( + R.string.access_code_check_warining_wait, + wrappedList(remainingSeconds), + ) + } else { + null + } + } + + suspend fun collectAttempts(attempts: Attempts) { + when (attempts) { + is Attempts.FastForward -> { + /** ignore */ + } + is Attempts.WithDelay -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + is Attempts.BeforeDeletion -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds) + ?: resourceReference( + R.string.access_code_check_warining_delete, + wrappedList(attempts.remainingAttemptsCountBeforeDeletion), + ), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + Attempts.Deletion -> deleteUserWallet() + } + } + + modelScope.launch { + hotAccessCodeAttemptsRepository.getAttempts(id) + .collectLatest { attempts -> collectAttempts(attempts) } + }.saveIn(attemptsRequestJobHolder) + } + + private suspend fun userWalletExists(id: HotWalletId): Boolean { + return userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot && it.hotWalletId == id } + } + + private suspend fun deleteUserWallet() { + val currentRequest = currentRequest.value ?: return + val userWallet = userWalletsListRepository.userWalletsSync() + .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return + userWalletsListRepository.delete(listOf(userWallet.walletId)) + dismiss() + } + private fun dismissState() { uiState.update { it.copy(isShown = false) } } - - private companion object { - const val ACCESS_CODE_LENGTH = 6 - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt index b1fb298d59..70bb695f83 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt @@ -1,8 +1,8 @@ package com.tangem.features.hotwallet.accesscoderequest.di import com.tangem.core.decompose.model.Model +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.HotAccessCodeRequestComponent -import com.tangem.features.hotwallet.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscoderequest.DefaultHotAccessCodeRequestComponent import com.tangem.features.hotwallet.accesscoderequest.HotAccessCodeRequestModel import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt index 82b7e51ec8..78c3c269f6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -1,9 +1,13 @@ package com.tangem.features.hotwallet.accesscoderequest.entity +import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference + internal data class HotAccessCodeRequestUM( val isShown: Boolean = false, val accessCode: String = "", - val wrongAccessCode: Boolean = false, + val accessCodeColor: PinTextColor = PinTextColor.Primary, + val wrongAccessCodeText: TextReference? = null, val useBiometricVisible: Boolean = true, val useBiometricClick: () -> Unit = {}, val onAccessCodeChange: (String) -> Unit = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt index 29a16c9c2f..1968bae87c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -1,6 +1,6 @@ package com.tangem.features.hotwallet.accesscoderequest.proxy -import com.tangem.features.hotwallet.HotWalletPasswordRequester +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -13,16 +13,15 @@ class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordR val componentRequester = MutableStateFlow(null) - override suspend fun wrongPassword() { - call { wrongPassword() } - } + override suspend fun wrongPassword() = call { wrongPassword() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result = - call { requestPassword(hasBiometry) } + override suspend fun successfulAuthentication() = call { successfulAuthentication() } - override suspend fun dismiss() { - call { dismiss() } - } + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result = call { requestPassword(attemptRequest) } + + override suspend fun dismiss() = call { dismiss() } private suspend fun call(block: suspend HotWalletPasswordRequester.() -> T): T { return withTimeout(timeMillis = 1000) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index bc64b660c4..2e3b61e43d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -13,6 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButton @@ -20,7 +22,10 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager @@ -85,9 +90,36 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM length = 6, isPasswordVisual = true, value = state.accessCode, - wrongCode = state.wrongAccessCode, + pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) + + SpacerH(20.dp) + + AnimatedVisibility( + modifier = Modifier.animateEnterExit( + enter = slideInVertically( + tween(), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween()), + exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), + ), + visible = state.wrongAccessCodeText != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + val wrongAccessCodeText = + state.wrongAccessCodeText ?: return@AnimatedVisibility + + Text( + text = wrongAccessCodeText.resolveReference(), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.warning, + ) + } } if (state.useBiometricVisible) { @@ -97,7 +129,10 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM .fillMaxWidth() .navigationBarsPadding() .imePadding(), - text = "Use biometric", + text = stringResourceSafe( + id = R.string.welcome_unlock, + stringResourceSafe(R.string.common_biometrics), + ), onClick = state.useBiometricClick, ) } @@ -106,9 +141,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM val hapticManager = LocalHapticManager.current - LaunchedEffect(state.wrongAccessCode) { - if (state.wrongAccessCode) { - hapticManager.perform(TangemHapticEffect.View.Reject) + LaunchedEffect(state.accessCodeColor) { + when (state.accessCodeColor) { + PinTextColor.WrongCode -> { + hapticManager.perform(TangemHapticEffect.View.Reject) + } + PinTextColor.Success -> { + hapticManager.perform(TangemHapticEffect.View.Confirm) + } + else -> Unit } } } @@ -125,7 +166,10 @@ private fun Preview() { var isShown by remember { mutableStateOf(true) } HotAccessCodeRequestFullScreenContent( - state = HotAccessCodeRequestUM(isShown = isShown), + state = HotAccessCodeRequestUM( + isShown = isShown, + wrongAccessCodeText = stringReference("Wrong access code"), + ), modifier = Modifier, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt new file mode 100644 index 0000000000..328e74cc8c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -0,0 +1,134 @@ +package com.tangem.features.hotwallet.addexistingwallet.entry + +import com.arkivanov.decompose.router.stack.* +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.ShouldAskPermissionUseCase +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent +import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class AddExistingWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, +) : Model() { + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() + val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() + val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks() + val accessCodeModelCallbacks = AccessCodeModelCallbacks() + val pushNotificationsCallbacks = PushNotificationsCallbacks() + val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = AddExistingWalletRoute.Start + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onChildBack() { + when (currentRoute.value) { + is AddExistingWalletRoute.Start -> router.pop() + is AddExistingWalletRoute.Import -> stackNavigation.pop() + is AddExistingWalletRoute.BackupCompleted -> Unit + is AddExistingWalletRoute.SetAccessCode -> Unit + is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop() + is AddExistingWalletRoute.PushNotifications -> Unit + is AddExistingWalletRoute.SetupFinished -> Unit + } + } + + private fun navigateToPushNotificationsOrNext() { + modelScope.launch { + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (shouldRequestPush) { + // is yet blocked by [REDACTED_TASK_KEY] + // stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) + stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) + } else { + stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) + } + } + } + + private fun navigateToSetupFinished() { + stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onChildBack() + } + + override fun onSkipClick() { + navigateToPushNotificationsOrNext() + } + } + + inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks { + override fun onBackClick() { + router.pop() + } + + override fun onImportPhraseClick() { + stackNavigation.push(AddExistingWalletRoute.Import) + } + } + + inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks { + override fun onWalletImported(userWalletId: UserWalletId) { + stackNavigation.replaceAll(AddExistingWalletRoute.BackupCompleted(userWalletId)) + } + } + + inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + stackNavigation.replaceAll(AddExistingWalletRoute.SetAccessCode(userWalletId)) + } + } + + inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { + override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode)) + } + + override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + navigateToPushNotificationsOrNext() + } + } + + inner class PushNotificationsCallbacks : PushNotificationsModelCallbacks { + override fun onAllowSystemPermission() { + navigateToSetupFinished() + } + + override fun onDenySystemPermission() { + navigateToSetupFinished() + } + + override fun onDismiss() { + navigateToSetupFinished() + } + } + + inner class MobileWalletSetupFinishedComponentModelCallbacks : + MobileWalletSetupFinishedComponent.ModelCallbacks { + override fun onContinueClick() { + router.replaceAll(AppRoute.Wallet) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt new file mode 100644 index 0000000000..14eabf9ebc --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt @@ -0,0 +1,79 @@ +package com.tangem.features.hotwallet.addexistingwallet.entry + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +internal class AddExistingWalletStepperStateManager { + + fun getStepperState(route: AddExistingWalletRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is AddExistingWalletRoute.Start -> null + + is AddExistingWalletRoute.Import -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_IMPORT, + steps = STEPS_COUNT, + title = resourceReference(R.string.wallet_import_seed_navtitle), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + + is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = false, + showSkipButton = true, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = true, + showSkipButton = true, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_NOTIFICATIONS, + steps = STEPS_COUNT, + title = resourceReference(R.string.onboarding_title_notifications), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_DONE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 5 + + private const val STEP_IMPORT = 1 + private const val STEP_BACKUP = 2 + private const val STEP_ACCESS_CODE = 3 + private const val STEP_NOTIFICATIONS = 4 + private const val STEP_DONE = 5 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt similarity index 53% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt index 1792942faf..e1e673e034 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.addexistingwallet.root +package com.tangem.features.hotwallet.addexistingwallet.entry import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable @@ -6,33 +6,36 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.hotwallet.AddExistingWalletComponent -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletChildFactory -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute -import com.tangem.features.hotwallet.addexistingwallet.root.ui.AddExistingWalletContent +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletChildFactory +import com.tangem.features.hotwallet.addexistingwallet.entry.ui.AddExistingWalletContent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch internal class DefaultAddExistingWalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: Unit, + private val stepperStateManager: AddExistingWalletStepperStateManager, addExistingWalletChildFactory: AddExistingWalletChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, ) : AddExistingWalletComponent, AppComponentContext by appComponentContext { private val model: AddExistingWalletModel = getOrCreateModel(params) - private val startRoute = AddExistingWalletRoute.Start - private val innerStack = childStack( key = "addExistingWalletInnerStack", source = model.stackNavigation, serializer = null, - initialConfiguration = startRoute, + initialConfiguration = model.startRoute, handleBackButton = true, childFactory = { configuration, factoryContext -> addExistingWalletChildFactory.createChild( @@ -43,27 +46,41 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( }, ) + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + @Composable override fun Content(modifier: Modifier) { val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onChildBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } - BackHandler(onBack = ::onChildBack) AddExistingWalletContent( stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, ) } - private fun onChildBack() { - val isEmptyStack = innerStack.value.backStack.isEmpty() - - if (isEmptyStack) { - router.pop() - } else { - val currentRoute = innerStack.value.active.configuration - model.onChildBack(currentRoute) - } - } - @AssistedFactory interface Factory : AddExistingWalletComponent.Factory { override fun create(context: AppComponentContext, params: Unit): DefaultAddExistingWalletComponent diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt new file mode 100644 index 0000000000..a8af84bae5 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt @@ -0,0 +1,53 @@ +package com.tangem.features.hotwallet.addexistingwallet.entry.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.AddExistingWalletComponent +import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel +import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletStepperStateManager +import com.tangem.features.hotwallet.addexistingwallet.entry.DefaultAddExistingWalletComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.HotWalletStepperModel +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AddExistingWalletModuleBinds { + + @Binds + @Singleton + fun bindAddExistingWalletComponentFactory( + impl: DefaultAddExistingWalletComponent.Factory, + ): AddExistingWalletComponent.Factory + + @Binds + @IntoMap + @ClassKey(AddExistingWalletModel::class) + fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model + + @Binds + fun bindFactory(impl: DefaultHotWalletStepperComponent.Factory): HotWalletStepperComponent.Factory + + @Binds + @IntoMap + @ClassKey(HotWalletStepperModel::class) + fun bindHotWalletStepperModel(model: HotWalletStepperModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object AddExistingWalletModule { + + @Provides + @Singleton + fun provideAddExistingWalletStepperStateManager(): AddExistingWalletStepperStateManager { + return AddExistingWalletStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt similarity index 52% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt index 8030308588..fb538425f2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.entity +package com.tangem.features.hotwallet.addexistingwallet.entry.entity internal data class AddExistingWalletUM( val onBackClick: () -> Unit, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt similarity index 68% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index a07186b664..de7e7a76f2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -1,21 +1,21 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.routing +package com.tangem.features.hotwallet.addexistingwallet.entry.routing import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent -import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent -import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.pushnotifications.api.PushNotificationsComponent -import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub import com.tangem.features.pushnotifications.api.PushNotificationsParams import javax.inject.Inject internal class AddExistingWalletChildFactory @Inject constructor( private val pushNotificationsComponent: PushNotificationsComponent.Factory, + private val accessCodeComponentFactory: AccessCodeComponent.Factory, ) { fun createChild( @@ -39,23 +39,35 @@ internal class AddExistingWalletChildFactory @Inject constructor( is AddExistingWalletRoute.BackupCompleted -> ManualBackupCompletedComponent( context = childContext, params = ManualBackupCompletedComponent.Params( + userWalletId = route.userWalletId, callbacks = model.manualBackupCompletedComponentModelCallbacks, ), ) - is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent( + is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create( context = childContext, - params = SetAccessCodeComponent.Params( + params = AccessCodeComponent.Params( + isConfirmMode = false, + userWalletId = route.userWalletId, + callbacks = model.accessCodeModelCallbacks, + ), + ) + is AddExistingWalletRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = true, + accessCodeToConfirm = route.accessCode, + userWalletId = route.userWalletId, callbacks = model.accessCodeModelCallbacks, ), ) is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create( context = childContext, params = PushNotificationsParams( - modelCallbacks = PushNotificationsModelCallbacksStub(), + modelCallbacks = model.pushNotificationsCallbacks, source = AppRoute.PushNotification.Source.Onboarding, ), ) - AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( + is AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( context = childContext, params = MobileWalletSetupFinishedComponent.Params( callbacks = model.mobileWalletSetupFinishedComponentModelCallbacks, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt similarity index 51% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt index c08f900605..c5f3009aac 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt @@ -1,6 +1,7 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.routing +package com.tangem.features.hotwallet.addexistingwallet.entry.routing import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable internal sealed class AddExistingWalletRoute : Route { @@ -12,10 +13,13 @@ internal sealed class AddExistingWalletRoute : Route { object Import : AddExistingWalletRoute() @Serializable - object BackupCompleted : AddExistingWalletRoute() + data class BackupCompleted(val userWalletId: UserWalletId) : AddExistingWalletRoute() @Serializable - object AccessCode : AddExistingWalletRoute() + data class SetAccessCode(val userWalletId: UserWalletId) : AddExistingWalletRoute() + + @Serializable + data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : AddExistingWalletRoute() @Serializable object PushNotifications : AddExistingWalletRoute() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt similarity index 54% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt index bc43867900..f0fd3f4adf 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt @@ -1,6 +1,7 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.ui +package com.tangem.features.hotwallet.addexistingwallet.entry.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding @@ -12,19 +13,29 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent @Composable -internal fun AddExistingWalletContent(stackState: ChildStack) { - Children( - stack = stackState, - animation = stackAnimation(slide()), +internal fun AddExistingWalletContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, +) { + Column( modifier = Modifier .background(color = TangemTheme.colors.background.primary) .fillMaxSize() .imePadding() .systemBarsPadding(), ) { - it.instance.Content(Modifier.fillMaxSize()) + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt index afd76f5987..096bd395a0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent import dagger.assisted.Assisted @@ -28,7 +29,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor( } interface ModelCallbacks { - fun onWalletImported() + fun onWalletImported(userWalletId: UserWalletId) } data class Params( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt index 420bd92d31..efd6c04702 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.entity import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -13,11 +12,10 @@ internal data class AddExistingWalletImportUM( val onPassphraseInfoClick: () -> Unit, val wordsErrorText: TextReference?, val invalidWords: ImmutableList, - val createWalletEnabled: Boolean, - val createWalletProgress: Boolean, - val createWalletClick: () -> Unit, + val importWalletEnabled: Boolean, + val importWalletProgress: Boolean, + val importWalletClick: () -> Unit, val suggestionsList: ImmutableList, val onSuggestionClick: (String) -> Unit, - val infoBottomSheetConfig: TangemBottomSheetConfig, val readyToImport: Boolean, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 2f70fa2273..60f3305500 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,29 +1,68 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.wallets.builder.HotUserWalletBuilder +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletImportModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val mnemonicRepository: MnemonicRepository, + private val tangemHotSdk: TangemHotSdk, + private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, + private val saveUserWalletUseCase: SaveWalletUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder + private val passphraseInfoAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_56) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.common_passphrase) + body = resourceReference(R.string.onboarding_bottom_sheet_passphrase_description) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + init { importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, @@ -36,6 +75,7 @@ internal class AddExistingWalletImportModel @Inject constructor( passphrase = passphrase, ) }, + onPassphraseInfoClick = ::onPassphraseInfoClick, ) } @@ -44,7 +84,43 @@ internal class AddExistingWalletImportModel @Inject constructor( @Suppress("UnusedPrivateMember") private fun importWallet(mnemonic: Mnemonic, passphrase: String?) { - // TODO implement importing seed phrase - params.callbacks.onWalletImported() + modelScope.launch { + setImportProgress(true) + + runCatching { + val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) + val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) + val userWallet = hotUserWalletBuilder.build() + saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true)) + .onLeft { + setImportProgress(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> { + uiMessageSender.send( + SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), + ) + } + } + } + .onRight { + setImportProgress(false) + params.callbacks.onWalletImported(userWallet.walletId) + } + }.onFailure { + Timber.e(it) + setImportProgress(false) + } + } + } + + private fun setImportProgress(progress: Boolean) { + uiState.update { + it.copy(importWalletProgress = progress) + } + } + + private fun onPassphraseInfoClick() { + uiMessageSender.send(passphraseInfoAlertBS) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index bc05c78e98..7f89025442 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult @@ -24,6 +23,7 @@ internal class ImportSeedPhraseUiStateBuilder( private val readyToImport: (Boolean) -> Unit, private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit, private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit, + private val onPassphraseInfoClick: () -> Unit, ) { private val wordsCheckJobHolder = JobHolder() private var importedMnemonic: Mnemonic? = null @@ -35,8 +35,8 @@ internal class ImportSeedPhraseUiStateBuilder( passPhrase = TextFieldValue(""), wordsErrorText = null, invalidWords = persistentListOf(), - createWalletEnabled = false, - createWalletProgress = false, + importWalletEnabled = false, + importWalletProgress = false, suggestionsList = persistentListOf(), wordsChange = { launchInterceptWords(wordsField = it) @@ -49,11 +49,10 @@ internal class ImportSeedPhraseUiStateBuilder( passphrase = it.text updateUiState { state -> state.copy(passPhrase = it) } }, - onPassphraseInfoClick = ::showInfoBS, - createWalletClick = ::onCreateWallet, + onPassphraseInfoClick = onPassphraseInfoClick, + importWalletClick = ::onCreateWallet, onSuggestionClick = { word -> addSuggestedWord(word) }, readyToImport = false, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, ) } @@ -115,7 +114,7 @@ internal class ImportSeedPhraseUiStateBuilder( updateUiState { it.copy( - createWalletEnabled = false, + importWalletEnabled = false, wordsErrorText = null, ) } @@ -130,7 +129,7 @@ internal class ImportSeedPhraseUiStateBuilder( it.copy( invalidWords = invalidWords.toImmutableList(), wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_wrong_words), - createWalletEnabled = false, + importWalletEnabled = false, ) } return @@ -143,7 +142,7 @@ internal class ImportSeedPhraseUiStateBuilder( it.copy( invalidWords = emptyList().toImmutableList(), wordsErrorText = null, - createWalletEnabled = true, + importWalletEnabled = true, ) } readyToImport(true) @@ -154,13 +153,13 @@ internal class ImportSeedPhraseUiStateBuilder( updateUiState { it.copy( wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_invalid_checksum), - createWalletEnabled = false, + importWalletEnabled = false, ) } } else { updateUiState { it.copy( - createWalletEnabled = false, + importWalletEnabled = false, wordsErrorText = null, ) } @@ -168,19 +167,6 @@ internal class ImportSeedPhraseUiStateBuilder( } } - private fun showInfoBS() { - updateUiState { state -> - state.copy( - infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty.copy( - isShown = true, - onDismissRequest = { - updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) } - }, - ), - ) - } - } - companion object { private const val MINIMUM_WORD_LENGTH = 2 private const val WORDS_INTERCEPT_DELAY_MS = 500L diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index 22cb0309c7..d7bb9338b0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -35,9 +35,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.OutlineTextFieldWithIcon -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation @@ -91,15 +90,14 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) } - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth(), text = stringResourceSafe(id = R.string.common_import), - iconResId = R.drawable.ic_tangem_24, - enabled = state.createWalletEnabled, - showProgress = state.createWalletProgress, - onClick = state.createWalletClick, + enabled = state.importWalletEnabled, + showProgress = state.importWalletProgress, + onClick = state.importWalletClick, ) } @@ -114,8 +112,6 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) } } - - PassphraseInfoBottomSheet(state.infoBottomSheetConfig) } @Composable @@ -229,12 +225,11 @@ private fun PreviewAddExistingWalletImportContent() { onPassphraseInfoClick = {}, wordsErrorText = null, invalidWords = persistentListOf(), - createWalletEnabled = false, - createWalletProgress = false, - createWalletClick = {}, + importWalletEnabled = false, + importWalletProgress = false, + importWalletClick = {}, suggestionsList = persistentListOf(), onSuggestionClick = {}, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, readyToImport = false, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt deleted file mode 100644 index 76521aab7a..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.hotwallet.addexistingwallet.im.port.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -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.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.primary, - ) { _: TangemBottomSheetConfigContent.Empty -> - PassphraseInfoBottomSheetContent(config.onDismissRequest) - } -} - -@Composable -fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - ) { - Icon( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = TangemTheme.dimens.size40) - .size(TangemTheme.dimens.size48), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - - Text( - text = stringResourceSafe(id = R.string.common_passphrase), - modifier = Modifier - .padding(top = TangemTheme.dimens.size40) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - - Text( - text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), - modifier = Modifier - .padding(top = TangemTheme.dimens.size16) - .padding(horizontal = TangemTheme.dimens.size24) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - ) - - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.size16) - .padding(top = TangemTheme.dimens.size40) - .padding(bottom = TangemTheme.dimens.size32) - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_ok), - onClick = onDismiss, - ) - } -} - -@Preview -@Composable -private fun PassphraseInfoBottomSheetContentPreview() { - TangemThemePreview { - PassphraseInfoBottomSheetContent({ }) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt deleted file mode 100644 index df8977d23d..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.tangem.features.hotwallet.addexistingwallet.root - -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.push -import com.arkivanov.decompose.router.stack.replaceCurrent -import com.tangem.common.routing.AppRoute -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.navigation.Router -import com.tangem.domain.settings.ShouldAskPermissionUseCase -import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute -import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent -import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent -import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent -import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -internal class AddExistingWalletModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val router: Router, - private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, -) : Model() { - - val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() - val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() - val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks() - val accessCodeModelCallbacks = AccessCodeModelCallbacks() - val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks() - - val stackNavigation = StackNavigation() - - fun onChildBack(currentRoute: AddExistingWalletRoute) { - when (currentRoute) { - AddExistingWalletRoute.Import -> stackNavigation.pop() - AddExistingWalletRoute.BackupCompleted -> Unit - AddExistingWalletRoute.AccessCode -> stackNavigation.pop() - AddExistingWalletRoute.PushNotifications -> Unit - AddExistingWalletRoute.SetupFinished -> Unit - AddExistingWalletRoute.Start -> Unit - } - } - - inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks { - override fun onBackClick() { - router.pop() - } - - override fun onImportPhraseClick() { - stackNavigation.push(AddExistingWalletRoute.Import) - } - } - - inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks { - override fun onWalletImported() { - stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted) - } - } - - inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { - override fun onContinueClick() { - stackNavigation.push(AddExistingWalletRoute.AccessCode) - } - } - - inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks { - override fun onBackClick() { - stackNavigation.pop() - } - - override fun onAccessCodeSet() { - modelScope.launch { - val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (shouldRequestPush) { - // is yet blocked by [REDACTED_TASK_KEY] - // stackNavigation.replaceCurrent(AddExistingWalletRoute.PushNotifications) - stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished) - } else { - stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished) - } - } - } - } - - inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks { - override fun onContinueClick() { - router.replaceAll(AppRoute.Wallet) - } - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt deleted file mode 100644 index 0c6a950236..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.di - -import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.AddExistingWalletComponent -import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel -import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface AddExistingWalletModule { - - @Binds - @Singleton - fun bindAddExistingWalletComponentFactory( - impl: DefaultAddExistingWalletComponent.Factory, - ): AddExistingWalletComponent.Factory - - @Binds - @IntoMap - @ClassKey(AddExistingWalletModel::class) - fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt index 58ea450355..dab15c9946 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -1,18 +1,63 @@ package com.tangem.features.hotwallet.addexistingwallet.start +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletStartModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val saveWalletUseCase: SaveWalletUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletStartComponent.Params = paramsContainer.require() @@ -20,10 +65,119 @@ internal class AddExistingWalletStartModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow( AddExistingWalletStartUM( + isScanInProgress = false, onBackClick = params.callbacks::onBackClick, onImportPhraseClick = params.callbacks::onImportPhraseClick, - onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ }, - onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ }, + onScanCardClick = ::onScanClick, + onBuyCardClick = ::onShopClick, ), ) + + private fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt index f898f9c1b7..37a5113f35 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.entity internal data class AddExistingWalletStartUM( + val isScanInProgress: Boolean, val onBackClick: () -> Unit, val onImportPhraseClick: () -> Unit, val onScanCardClick: () -> Unit, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index eef14e30c9..6bf693e36b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -74,14 +75,25 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi title = stringResourceSafe(R.string.wallet_import_scan_title), description = stringResourceSafe(R.string.wallet_import_scan_description), badge = { - Icon( - modifier = Modifier - .padding(top = 2.dp) - .size(20.dp), - painter = painterResource(R.drawable.ic_tangem_24), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) + if (state.isScanInProgress) { + CircularProgressIndicator( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp) + .padding(2.dp), + color = TangemTheme.colors.text.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } else { + Icon( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp), + painter = painterResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + } }, onClick = state.onScanCardClick, enabled = true, @@ -160,6 +172,7 @@ private fun PreviewCreateWalletContent() { TangemThemePreview { AddExistingWalletStartContent( state = AddExistingWalletStartUM( + isScanInProgress = true, onBackClick = {}, onImportPhraseClick = {}, onScanCardClick = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index ad1d8637e1..8e90402e09 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme -internal const val DISABLED_COLORS_ALPHA = 0.5f +private const val DISABLED_COLORS_ALPHA = 0.5f @Suppress("LongParameterList") @Composable diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index a4a7a4f243..0b3261b4f3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -45,10 +45,9 @@ internal class CreateMobileWalletModel @Inject constructor( runCatching { val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) - saveUserWalletUseCase( - hotUserWalletBuilder.build(), - ) - router.push(AppRoute.Wallet) + val userWallet = hotUserWalletBuilder.build() + saveUserWalletUseCase(userWallet) + router.replaceAll(AppRoute.Wallet) }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt new file mode 100644 index 0000000000..8cc687e476 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt @@ -0,0 +1,97 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ModelScoped +internal class CreateWalletBackupModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + val params = paramsContainer.require() + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks() + val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks() + val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks() + val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onBack() { + when (currentRoute.value) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop() + is CreateWalletBackupRoute.RecoveryPhrase -> stackNavigation.pop() + is CreateWalletBackupRoute.ConfirmBackup -> stackNavigation.pop() + is CreateWalletBackupRoute.BackupCompleted -> router.pop() + } + } + + fun onManualBackupStarted() { + stackNavigation.push(CreateWalletBackupRoute.RecoveryPhrase) + } + + fun onManualBackupPhraseShown() { + stackNavigation.push(CreateWalletBackupRoute.ConfirmBackup) + } + + fun onManualBackupChecked() { + stackNavigation.push(CreateWalletBackupRoute.BackupCompleted) + } + + fun onManualBackupCompleted() { + router.pop() + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onBack() + } + + override fun onSkipClick() = Unit + } + + inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupStarted() + } + } + + inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupPhraseShown() + } + } + + inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks { + override fun onCompleteClick() { + onManualBackupChecked() + } + } + + inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + onManualBackupCompleted() + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt new file mode 100644 index 0000000000..5b8c3a2e68 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt @@ -0,0 +1,56 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import javax.inject.Inject + +internal class CreateWalletBackupStepperStateManager @Inject constructor() { + + fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_START, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_PHRASE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_CONFIRM, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_COMPLETED, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 4 + + private const val STEP_START = 1 + private const val STEP_PHRASE = 2 + private const val STEP_CONFIRM = 3 + private const val STEP_COMPLETED = 4 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt new file mode 100644 index 0000000000..b13c9193f9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory +import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultCreateWalletBackupComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: CreateWalletBackupComponent.Params, + private val stepperStateManager: CreateWalletBackupStepperStateManager, + createWalletBackupChildFactory: CreateWalletBackupChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, +) : CreateWalletBackupComponent, AppComponentContext by appComponentContext { + + private val model: CreateWalletBackupModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "createWalletBackupInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createWalletBackupChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + + CreateWalletBackupContent( + stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : CreateWalletBackupComponent.Factory { + override fun create( + context: AppComponentContext, + params: CreateWalletBackupComponent.Params, + ): DefaultCreateWalletBackupComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt new file mode 100644 index 0000000000..5ea687703d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager +import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateWalletBackupModuleBinds { + + @Binds + @Singleton + fun bindCreateWalletBackupComponentFactory( + impl: DefaultCreateWalletBackupComponent.Factory, + ): CreateWalletBackupComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateWalletBackupModel::class) + fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateWalletBackupModule { + + @Provides + @Singleton + fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager { + return CreateWalletBackupStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt new file mode 100644 index 0000000000..44d740af6c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import javax.inject.Inject + +internal class CreateWalletBackupChildFactory @Inject constructor() { + + fun createChild( + route: CreateWalletBackupRoute, + childContext: AppComponentContext, + model: CreateWalletBackupModel, + ): ComposableContentComponent = when (route) { + CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent( + context = childContext, + params = ManualBackupStartComponent.Params( + callbacks = model.manualBackupStartModelCallbacks, + ), + ) + CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent( + context = childContext, + params = ManualBackupPhraseComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupPhraseModelCallbacks, + ), + ) + CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent( + context = childContext, + params = ManualBackupCheckComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCheckModelCallbacks, + ), + ) + CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent( + context = childContext, + params = ManualBackupCompletedComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCompletedModelCallbacks, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt new file mode 100644 index 0000000000..f063a1f796 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt @@ -0,0 +1,19 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface CreateWalletBackupRoute { + + @Serializable + data object RecoveryPhraseStart : CreateWalletBackupRoute + + @Serializable + data object RecoveryPhrase : CreateWalletBackupRoute + + @Serializable + data object ConfirmBackup : CreateWalletBackupRoute + + @Serializable + data object BackupCompleted : CreateWalletBackupRoute +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt new file mode 100644 index 0000000000..e6f1e9e732 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +@Composable +internal fun CreateWalletBackupContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt index dfaf6fe787..7f25916f2d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent import dagger.assisted.Assisted @@ -33,7 +33,7 @@ internal class ManualBackupCheckComponent @AssistedInject constructor( } data class Params( - val generatedWords: Mnemonic, + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt new file mode 100644 index 0000000000..34c8d3bb8c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.manualbackup.check.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManualBackupCheckModule { + + @Binds + @IntoMap + @ClassKey(ManualBackupCheckModel::class) + fun bindManualBackupCheckModel(model: ManualBackupCheckModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt index 19eac1ac68..5d2bc48d20 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt @@ -2,10 +2,12 @@ package com.tangem.features.hotwallet.manualbackup.check.entity import androidx.compose.ui.text.input.TextFieldValue import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf internal data class ManualBackupCheckUM( val onCompleteButtonClick: () -> Unit, val wordFields: ImmutableList, + val words: ImmutableList = persistentListOf(), val completeButtonEnabled: Boolean, val completeButtonProgress: Boolean, ) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt index ffe2a0ad15..e0778baa69 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt @@ -2,16 +2,25 @@ package com.tangem.features.hotwallet.manualbackup.check.model import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.Boolean import kotlin.Int @@ -20,13 +29,15 @@ import kotlin.Suppress import kotlin.collections.List import kotlin.collections.all import kotlin.collections.map -import kotlin.error @Stable @ModelScoped internal class ManualBackupCheckModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val tangemHotSdk: TangemHotSdk, ) : Model() { private val params = paramsContainer.require() @@ -35,6 +46,28 @@ internal class ManualBackupCheckModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow(getInitialUIState()) + init { + modelScope.launch { + runCatching { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + val seedPhrasePrivateInfo = tangemHotSdk.exportMnemonic(unlockHotWallet) + uiState.update { + it.copy( + words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -> + WORD_FIELD_INDICES.contains(index + 1) + }.toImmutableList(), + ) + } + } + }.onFailure { + Timber.e(it) + } + } + } + @Suppress("MagicNumber") private fun getInitialUIState(): ManualBackupCheckUM { val wordFields = List(WORD_FIELD_INDICES.size) { index -> @@ -57,12 +90,40 @@ internal class ManualBackupCheckModel @Inject constructor( onCompleteButtonClick = { val currentUIState = uiState.value if (currentUIState.completeButtonEnabled) { - callbacks.onCompleteClick() + backupWallet() } }, ) } + private fun backupWallet() { + modelScope.launch { + uiState.update { + it.copy(completeButtonProgress = true) + } + + runCatching { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + updateWalletUseCase(userWallet.walletId) { + userWallet.copy(backedUp = true) + } + callbacks.onCompleteClick() + } + uiState.update { + it.copy(completeButtonProgress = false) + } + }.onFailure { + Timber.e(it) + + uiState.update { + it.copy(completeButtonProgress = false) + } + } + } + } + private fun updateWordField(shownIndex: Int, newText: TextFieldValue) { uiState.update { currentState -> val updatedFields = currentState.wordFields.map { wordField -> @@ -89,14 +150,9 @@ internal class ManualBackupCheckModel @Inject constructor( } private fun checkWordField(word: String, shownIndex: Int): Boolean { - val generatedWords = params.generatedWords - val wordList = generatedWords.mnemonicComponents - - return if (shownIndex <= wordList.size) { - wordList[shownIndex - 1] == word - } else { - false - } + val words = uiState.value.words + val listIndex = WORD_FIELD_INDICES.indexOf(shownIndex) + return words.getOrNull(listIndex)?.let { word == it } == true } companion object { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt index 43f8c076d3..bd40e6bb1a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -27,10 +28,11 @@ internal class ManualBackupCompletedComponent @AssistedInject constructor( } interface ModelCallbacks { - fun onContinueClick() + fun onContinueClick(userWalletId: UserWalletId) } data class Params( + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt index 0646d7646b..d0fcfe7333 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt @@ -20,7 +20,7 @@ internal class ManualBackupCompletedModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow( ManualBackupCompletedUM( - onContinueClick = params.callbacks::onContinueClick, + onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) }, ), ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt index 1dd8b360fc..62d7b79ed4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel import com.tangem.features.hotwallet.manualbackup.phrase.ui.ManualBackupPhraseContent import dagger.assisted.Assisted @@ -32,6 +33,7 @@ internal class ManualBackupPhraseComponent @AssistedInject constructor( } data class Params( + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt new file mode 100644 index 0000000000..3b5c8c8018 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.manualbackup.phrase.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManualBackupPhraseModule { + + @Binds + @IntoMap + @ClassKey(ManualBackupPhraseModel::class) + fun bindManualBackupPhraseModel(model: ManualBackupPhraseModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt index 4320a86026..e9625260ad 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt @@ -1,14 +1,24 @@ package com.tangem.features.hotwallet.manualbackup.phrase.model import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @@ -16,17 +26,39 @@ import javax.inject.Inject internal class ManualBackupPhraseModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val tangemHotSdk: TangemHotSdk, ) : Model() { private val params = paramsContainer.require() private val callbacks = params.callbacks internal val uiState: StateFlow - field = MutableStateFlow(getInitialUIState()) - - private fun getInitialUIState(): ManualBackupPhraseUM { - return ManualBackupPhraseUM( + field = MutableStateFlow( + ManualBackupPhraseUM( onContinueClick = callbacks::onContinueClick, - ) + ), + ) + + init { + modelScope.launch { + runCatching { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + val seedPhrasePrivateInfo = tangemHotSdk.exportMnemonic(unlockHotWallet) + uiState.update { + it.copy( + words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> + ManualBackupPhraseUM.MnemonicGridItem(index + 1, s) + }.toImmutableList(), + ) + } + } + }.onFailure { + Timber.e(it) + } + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt new file mode 100644 index 0000000000..2f506e7b30 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.manualbackup.start.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManualBackupStartModule { + + @Binds + @IntoMap + @ClassKey(ManualBackupStartModel::class) + fun bindManualBackupStartModel(model: ManualBackupStartModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt deleted file mode 100644 index 945ff9ad20..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import javax.inject.Inject - -@Stable -@ModelScoped -internal class SetAccessCodeModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - internal val uiState: StateFlow - field = MutableStateFlow(getInitialState()) - - fun onBack() { - when (uiState.value.step) { - SetAccessCodeUM.Step.AccessCode -> { - params.callbacks.onBackClick() - } - SetAccessCodeUM.Step.ConfirmAccessCode -> { - uiState.update { - it.copy( - step = SetAccessCodeUM.Step.AccessCode, - accessCodeSecond = "", - ) - } - } - } - } - - private fun getInitialState() = SetAccessCodeUM( - step = SetAccessCodeUM.Step.AccessCode, - accessCodeFirst = "", - accessCodeSecond = "", - onAccessCodeFirstChange = ::onAccessCodeFirstChange, - onAccessCodeSecondChange = ::onAccessCodeSecondChange, - buttonEnabled = false, - onContinue = ::onContinue, - ) - - private fun onAccessCodeFirstChange(value: String) { - uiState.update { - it.copy( - accessCodeFirst = value, - buttonEnabled = value.length == uiState.value.accessCodeLength, - ) - } - } - - private fun onAccessCodeSecondChange(value: String) { - uiState.update { - it.copy( - accessCodeSecond = value, - buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond, - ) - } - } - - private fun onContinue() { - when (uiState.value.step) { - SetAccessCodeUM.Step.AccessCode -> { - uiState.update { - it.copy( - step = SetAccessCodeUM.Step.ConfirmAccessCode, - ) - } - } - SetAccessCodeUM.Step.ConfirmAccessCode -> { - params.callbacks.onAccessCodeSet() - } - } - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt deleted file mode 100644 index a5e98ca830..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.entity - -internal data class SetAccessCodeUM( - val step: Step, - val accessCodeFirst: String, - val accessCodeSecond: String, - val onAccessCodeFirstChange: (String) -> Unit, - val onAccessCodeSecondChange: (String) -> Unit, - val buttonEnabled: Boolean, - val onContinue: () -> Unit, -) { - val accessCodeLength: Int = ACCESS_CODE_LENGTH - - enum class Step { - AccessCode, - ConfirmAccessCode, - } - - companion object { - private const val ACCESS_CODE_LENGTH = 6 - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt deleted file mode 100644 index 64449365ae..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemAnimations -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.* -import com.tangem.core.res.R - -@Composable -internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) { - BackHandler(onBack = onBack) - - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - ) { - AnimatedContent( - modifier = Modifier.weight(1f), - targetState = state.step, - transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> target.ordinal > initial.ordinal }, - label = "AnimatedContent", - ) { step -> - when (step) { - AccessCode -> SetAccessCodeEnter( - modifier = Modifier.padding(top = 16.dp), - state = state, - reEnterAccessCodeState = false, - ) - ConfirmAccessCode -> SetAccessCodeEnter( - modifier = Modifier.padding(top = 16.dp), - state = state, - reEnterAccessCodeState = true, - ) - } - } - - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .imePadding(), - text = if (state.step == ConfirmAccessCode) { - stringResourceSafe(R.string.common_confirm) - } else { - stringResourceSafe(R.string.common_continue) - }, - onClick = state.onContinue, - ) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt deleted file mode 100644 index bd787c1822..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.res.R -import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM - -@Composable -internal fun SetAccessCodeEnter( - state: SetAccessCodeUM, - reEnterAccessCodeState: Boolean, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize() - .background(TangemTheme.colors.background.primary) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier - .padding(top = 56.dp) - .align(Alignment.CenterHorizontally), - text = if (reEnterAccessCodeState) { - stringResourceSafe(R.string.access_code_confirm_title) - } else { - stringResourceSafe(R.string.access_code_create_title) - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Text( - modifier = Modifier - .padding(16.dp) - .align(Alignment.CenterHorizontally), - text = if (reEnterAccessCodeState) { - stringResourceSafe(R.string.access_code_confirm_description) - } else { - stringResourceSafe( - R.string.access_code_create_description, - state.accessCodeLength, - ) - }, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - contentAlignment = Alignment.Center, - ) { - PinTextField( - length = state.accessCodeLength, - isPasswordVisual = true, - value = if (reEnterAccessCodeState) { - state.accessCodeSecond - } else { - state.accessCodeFirst - }, - onValueChange = if (reEnterAccessCodeState) { - state.onAccessCodeSecondChange - } else { - state.onAccessCodeFirstChange - }, - ) - } - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - SetAccessCodeEnter( - reEnterAccessCodeState = false, - state = SetAccessCodeUM( - step = SetAccessCodeUM.Step.AccessCode, - accessCodeFirst = "", - accessCodeSecond = "", - onAccessCodeFirstChange = {}, - onAccessCodeSecondChange = {}, - buttonEnabled = false, - onContinue = {}, - ), - ) - } -} - -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview2() { - TangemThemePreview { - SetAccessCodeEnter( - reEnterAccessCodeState = true, - state = SetAccessCodeUM( - step = SetAccessCodeUM.Step.ConfirmAccessCode, - accessCodeFirst = "", - accessCodeSecond = "", - onAccessCodeFirstChange = {}, - onAccessCodeSecondChange = {}, - buttonEnabled = false, - onContinue = {}, - ), - ) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt new file mode 100644 index 0000000000..169d037699 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.hotwallet.stepper.api + +import androidx.annotation.IntRange +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.TextReference +import kotlinx.coroutines.flow.StateFlow + +interface HotWalletStepperComponent : ComposableContentComponent { + + data class StepperUM( + @IntRange(from = 0) val currentStep: Int, + @IntRange(from = 0) val steps: Int, + val title: TextReference, + val showBackButton: Boolean, + val showSkipButton: Boolean, + val showFeedbackButton: Boolean, + ) { + companion object { + fun initialState() = StepperUM( + currentStep = 0, + steps = 0, + title = TextReference.EMPTY, + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + interface ModelCallback { + fun onBackClick() + fun onSkipClick() + } + + class Params( + val initState: StepperUM, + val callback: ModelCallback, + ) + + val state: StateFlow + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt new file mode 100644 index 0000000000..211c9265f3 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.hotwallet.stepper.di + +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface HotWalletStepperModule { + + @Binds + fun bindHotWalletStepperComponentFactory( + impl: DefaultHotWalletStepperComponent.Factory, + ): HotWalletStepperComponent.Factory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt new file mode 100644 index 0000000000..320920105b --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.hotwallet.stepper.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.ui.HotWalletStepper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultHotWalletStepperComponent @AssistedInject constructor( + @Assisted val context: AppComponentContext, + @Assisted val params: HotWalletStepperComponent.Params, +) : HotWalletStepperComponent, AppComponentContext by context { + + private val model: HotWalletStepperModel = getOrCreateModel(params) + + override val state = model.uiState + + fun updateState(newState: HotWalletStepperComponent.StepperUM) { + model.updateState(newState) + } + + @Composable + override fun Content(modifier: Modifier) { + val uiState by state.collectAsStateWithLifecycle() + + HotWalletStepper( + state = uiState, + modifier = modifier, + onBackClick = model::onBackClick, + onSkipClick = model::onSkipClick, + onFeedbackClick = model::onFeedbackClick, + ) + } + + @AssistedFactory + interface Factory : HotWalletStepperComponent.Factory { + override fun create( + context: AppComponentContext, + params: HotWalletStepperComponent.Params, + ): DefaultHotWalletStepperComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt new file mode 100644 index 0000000000..ed64c2fb93 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.stepper.impl + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class HotWalletStepperModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(params.initState) + + fun updateState(newState: HotWalletStepperComponent.StepperUM) { + uiState.value = newState + } + + fun onBackClick() { + params.callback.onBackClick() + } + + fun onSkipClick() { + // TODO send analytics + params.callback.onSkipClick() + } + + fun onFeedbackClick() { + // TODO send analytics + // openFeedback() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt new file mode 100644 index 0000000000..ea029d37be --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt @@ -0,0 +1,106 @@ +package com.tangem.features.hotwallet.stepper.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +@Composable +internal fun HotWalletStepper( + state: HotWalletStepperComponent.StepperUM, + onBackClick: () -> Unit, + onSkipClick: () -> Unit, + onFeedbackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1) + val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(targetFraction = fraction) + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemTopAppBar( + startButton = if (state.showBackButton) { + TopAppBarButtonUM.Back(onBackClick) + } else { + null + }, + endButton = when { + state.showSkipButton -> TopAppBarButtonUM.Text( + text = resourceReference(R.string.common_skip), + onClicked = onSkipClick, + ) + state.showFeedbackButton -> TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_chat_24, + onClicked = onFeedbackClick, + ) + else -> null + }, + title = state.title, + containerColor = TangemTheme.colors.background.primary, + modifier = modifier, + titleAlignment = Alignment.CenterHorizontally, + ) + + TangemLinearProgressIndicator( + modifier = Modifier + .padding(horizontal = 16.dp) + .height(4.dp) + .fillMaxWidth(), + progress = { animatedIndicatorFraction }, + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f), + strokeCap = StrokeCap.Round, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun HotWalletStepper_Preview() { + TangemThemePreview { + Box( + Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + HotWalletStepper( + modifier = Modifier.align(Alignment.TopCenter), + state = HotWalletStepperComponent.StepperUM( + currentStep = 2, + steps = 3, + title = resourceReference(R.string.common_done), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ), + onBackClick = {}, + onSkipClick = {}, + onFeedbackClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt new file mode 100644 index 0000000000..127f506d0c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt @@ -0,0 +1,60 @@ +package com.tangem.features.hotwallet.updateaccesscode + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeChildFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultUpdateAccessCodeComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: UpdateAccessCodeComponent.Params, + private val childFactory: UpdateAccessCodeChildFactory, +) : UpdateAccessCodeComponent, AppComponentContext by appComponentContext { + + private val model: UpdateAccessCodeModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "hotWalletAccessCodeInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + childFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + + BackHandler(onBack = model::onChildBack) + + SetAccessCodeContent( + onBackClick = model::onChildBack, + stackState = stackState, + ) + } + + @AssistedFactory + interface Factory : UpdateAccessCodeComponent.Factory { + override fun create( + context: AppComponentContext, + params: UpdateAccessCodeComponent.Params, + ): DefaultUpdateAccessCodeComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt new file mode 100644 index 0000000000..e8f3c4a40d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.updateaccesscode + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeRoute + +@Composable +internal fun SetAccessCodeContent( + onBackClick: () -> Unit, + stackState: ChildStack, +) { + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier, + title = stringResourceSafe(R.string.access_code_navtitle), + startButton = TopAppBarButtonUM.Back(onBackClick), + ) + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt new file mode 100644 index 0000000000..19f8947793 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -0,0 +1,45 @@ +package com.tangem.features.hotwallet.updateaccesscode + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeRoute +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ModelScoped +internal class UpdateAccessCodeModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model(), AccessCodeComponent.ModelCallbacks { + + private val params = paramsContainer.require() + + val stackNavigation = StackNavigation() + val startRoute: UpdateAccessCodeRoute = UpdateAccessCodeRoute.SetAccessCode(params.userWalletId) + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onChildBack() { + when (currentRoute.value) { + is UpdateAccessCodeRoute.SetAccessCode -> router.pop() + is UpdateAccessCodeRoute.ConfirmAccessCode -> stackNavigation.pop() + } + } + + override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode)) + } + + override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + router.pop() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt new file mode 100644 index 0000000000..478deec433 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.hotwallet.updateaccesscode.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.DefaultUpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.UpdateAccessCodeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface UpdateAccessCodeModule { + + @Binds + fun bindUpdateAccessCodeComponentFactory( + impl: DefaultUpdateAccessCodeComponent.Factory, + ): UpdateAccessCodeComponent.Factory + + @Binds + @IntoMap + @ClassKey(UpdateAccessCodeModel::class) + fun bindUpdateAccessCodeModel(model: UpdateAccessCodeModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt new file mode 100644 index 0000000000..354156a4c0 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt @@ -0,0 +1,38 @@ +package com.tangem.features.hotwallet.updateaccesscode.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.updateaccesscode.UpdateAccessCodeModel +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import javax.inject.Inject + +internal class UpdateAccessCodeChildFactory @Inject constructor( + private val accessCodeComponentFactory: AccessCodeComponent.Factory, +) { + + fun createChild( + route: UpdateAccessCodeRoute, + childContext: AppComponentContext, + model: UpdateAccessCodeModel, + ): ComposableContentComponent { + return when (route) { + is UpdateAccessCodeRoute.SetAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = false, + userWalletId = route.userWalletId, + callbacks = model, + ), + ) + is UpdateAccessCodeRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = true, + accessCodeToConfirm = route.accessCode, + userWalletId = route.userWalletId, + callbacks = model, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt new file mode 100644 index 0000000000..a414995364 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet.updateaccesscode.routing + +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +internal sealed class UpdateAccessCodeRoute : Route { + + @Serializable + data class SetAccessCode(val userWalletId: UserWalletId) : UpdateAccessCodeRoute() + + @Serializable + data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : UpdateAccessCodeRoute() +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt new file mode 100644 index 0000000000..f24a1ea128 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.hotwallet.walletactivation.entry + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationChildFactory +import com.tangem.features.hotwallet.walletactivation.entry.ui.WalletActivationContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultWalletActivationComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: WalletActivationComponent.Params, + private val stepperStateManager: WalletActivationStepperStateManager, + walletActivationChildFactory: WalletActivationChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, +) : WalletActivationComponent, AppComponentContext by appComponentContext { + + private val model: WalletActivationModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "walletActivationInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + walletActivationChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onChildBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + + WalletActivationContent( + stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : WalletActivationComponent.Factory { + override fun create( + context: AppComponentContext, + params: WalletActivationComponent.Params, + ): DefaultWalletActivationComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt new file mode 100644 index 0000000000..f163ba6e18 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -0,0 +1,147 @@ +package com.tangem.features.hotwallet.walletactivation.entry + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.router.stack.replaceAll +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.ShouldAskPermissionUseCase +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class WalletActivationModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, +) : Model() { + + val params = paramsContainer.require() + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks() + val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks() + val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks() + val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks() + val accessCodeModelCallbacks = AccessCodeModelCallbacks() + val pushNotificationsCallbacks = PushNotificationsCallbacks() + val mobileWalletSetupFinishedModelCallbacks = MobileWalletSetupFinishedModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = WalletActivationRoute.ManualBackupStart + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onChildBack() { + when (currentRoute.value) { + is WalletActivationRoute.ManualBackupStart -> router.pop() + is WalletActivationRoute.ManualBackupPhrase -> stackNavigation.pop() + is WalletActivationRoute.ManualBackupCheck -> stackNavigation.pop() + is WalletActivationRoute.ManualBackupCompleted -> Unit + is WalletActivationRoute.SetAccessCode -> Unit + is WalletActivationRoute.ConfirmAccessCode -> Unit + is WalletActivationRoute.PushNotifications -> Unit + is WalletActivationRoute.SetupFinished -> Unit + } + } + + private fun navigateToPushNotificationsOrNext() { + modelScope.launch { + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (shouldRequestPush) { + // is yet blocked by [REDACTED_TASK_KEY] + // stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) + stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) + } else { + stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) + } + } + } + + private fun navigateToSetupFinished() { + stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onChildBack() + } + + override fun onSkipClick() { + navigateToPushNotificationsOrNext() + } + } + + inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks { + override fun onContinueClick() { + stackNavigation.push(WalletActivationRoute.ManualBackupPhrase) + } + } + + inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks { + override fun onContinueClick() { + stackNavigation.push( + WalletActivationRoute.ManualBackupCheck, + ) + } + } + + inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks { + override fun onCompleteClick() { + stackNavigation.push(WalletActivationRoute.ManualBackupCompleted) + } + } + + inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + stackNavigation.push(WalletActivationRoute.SetAccessCode) + } + } + + inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { + override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) + } + + override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + navigateToPushNotificationsOrNext() + } + } + + inner class PushNotificationsCallbacks : PushNotificationsModelCallbacks { + override fun onAllowSystemPermission() { + navigateToSetupFinished() + } + + override fun onDenySystemPermission() { + navigateToSetupFinished() + } + + override fun onDismiss() { + navigateToSetupFinished() + } + } + + inner class MobileWalletSetupFinishedModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks { + override fun onContinueClick() { + router.pop() + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt new file mode 100644 index 0000000000..5a33b36b87 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt @@ -0,0 +1,91 @@ +package com.tangem.features.hotwallet.walletactivation.entry + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute +import javax.inject.Inject + +internal class WalletActivationStepperStateManager @Inject constructor() { + + fun getStepperState(route: WalletActivationRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is WalletActivationRoute.ManualBackupStart -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is WalletActivationRoute.ManualBackupPhrase -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP_PHRASE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is WalletActivationRoute.ManualBackupCheck -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP_CHECK, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is WalletActivationRoute.ManualBackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP_COMPLETED, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + is WalletActivationRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = false, + showSkipButton = true, + showFeedbackButton = false, + ) + is WalletActivationRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = false, + ) + is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_NOTIFICATIONS, + steps = STEPS_COUNT, + title = resourceReference(R.string.onboarding_title_notifications), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + is WalletActivationRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_DONE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 7 + + private const val STEP_BACKUP = 1 + private const val STEP_BACKUP_PHRASE = 2 + private const val STEP_BACKUP_CHECK = 3 + private const val STEP_BACKUP_COMPLETED = 4 + private const val STEP_ACCESS_CODE = 5 + private const val STEP_NOTIFICATIONS = 6 + private const val STEP_DONE = 7 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt new file mode 100644 index 0000000000..51362cff09 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.walletactivation.entry.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.walletactivation.entry.DefaultWalletActivationComponent +import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel +import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationStepperStateManager +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletActivationModuleBinds { + + @Binds + @Singleton + fun bindWalletActivationComponentFactory( + impl: DefaultWalletActivationComponent.Factory, + ): WalletActivationComponent.Factory + + @Binds + @IntoMap + @ClassKey(WalletActivationModel::class) + fun bindWalletActivationModel(model: WalletActivationModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object WalletActivationModule { + + @Provides + @Singleton + fun provideWalletActivationStepperStateManager(): WalletActivationStepperStateManager { + return WalletActivationStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt new file mode 100644 index 0000000000..e6c13bdbd9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt @@ -0,0 +1,87 @@ +package com.tangem.features.hotwallet.walletactivation.entry.routing + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent +import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel +import com.tangem.features.pushnotifications.api.PushNotificationsComponent +import com.tangem.features.pushnotifications.api.PushNotificationsParams +import javax.inject.Inject + +internal class WalletActivationChildFactory @Inject constructor( + private val pushNotificationsComponent: PushNotificationsComponent.Factory, + private val accessCodeComponentFactory: AccessCodeComponent.Factory, +) { + + fun createChild( + route: WalletActivationRoute, + childContext: AppComponentContext, + model: WalletActivationModel, + ): ComposableContentComponent { + return when (route) { + is WalletActivationRoute.ManualBackupStart -> ManualBackupStartComponent( + context = childContext, + params = ManualBackupStartComponent.Params( + callbacks = model.manualBackupStartModelCallbacks, + ), + ) + is WalletActivationRoute.ManualBackupPhrase -> ManualBackupPhraseComponent( + context = childContext, + params = ManualBackupPhraseComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupPhraseModelCallbacks, + ), + ) + is WalletActivationRoute.ManualBackupCheck -> ManualBackupCheckComponent( + context = childContext, + params = ManualBackupCheckComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCheckModelCallbacks, + ), + ) + is WalletActivationRoute.ManualBackupCompleted -> ManualBackupCompletedComponent( + context = childContext, + params = ManualBackupCompletedComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCompletedModelCallbacks, + ), + ) + is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = false, + userWalletId = model.params.userWalletId, + callbacks = model.accessCodeModelCallbacks, + ), + ) + is WalletActivationRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = true, + accessCodeToConfirm = route.accessCode, + userWalletId = model.params.userWalletId, + callbacks = model.accessCodeModelCallbacks, + ), + ) + is WalletActivationRoute.PushNotifications -> pushNotificationsComponent.create( + context = childContext, + params = PushNotificationsParams( + modelCallbacks = model.pushNotificationsCallbacks, + source = AppRoute.PushNotification.Source.Onboarding, + ), + ) + is WalletActivationRoute.SetupFinished -> MobileWalletSetupFinishedComponent( + context = childContext, + params = MobileWalletSetupFinishedComponent.Params( + callbacks = model.mobileWalletSetupFinishedModelCallbacks, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt new file mode 100644 index 0000000000..26b6998a2e --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt @@ -0,0 +1,31 @@ +package com.tangem.features.hotwallet.walletactivation.entry.routing + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +internal sealed class WalletActivationRoute : Route { + + @Serializable + object ManualBackupStart : WalletActivationRoute() + + @Serializable + object ManualBackupPhrase : WalletActivationRoute() + + @Serializable + data object ManualBackupCheck : WalletActivationRoute() + + @Serializable + object ManualBackupCompleted : WalletActivationRoute() + + @Serializable + data object SetAccessCode : WalletActivationRoute() + + @Serializable + data class ConfirmAccessCode(val accessCode: String) : WalletActivationRoute() + + @Serializable + object PushNotifications : WalletActivationRoute() + + @Serializable + object SetupFinished : WalletActivationRoute() +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt new file mode 100644 index 0000000000..0c2e2719e9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.walletactivation.entry.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute + +@Composable +internal fun WalletActivationContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index be22d8cd38..f3f17cd9d3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -1,11 +1,14 @@ package com.tangem.features.hotwallet.walletbackup.entity +import com.tangem.core.ui.components.label.entity.LabelUM + internal data class WalletBackupUM( val onBackClick: () -> Unit, - val recoveryPhraseStatus: BackupStatus, - val googleDriveStatus: BackupStatus, + val recoveryPhraseStatus: LabelUM?, + val googleDriveStatus: LabelUM?, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, + val backedUp: Boolean, ) internal sealed class BackupStatus { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index f85e281c17..b398ed047a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -1,12 +1,25 @@ package com.tangem.features.hotwallet.walletbackup.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.common.routing.AppRoute import com.tangem.features.hotwallet.WalletBackupComponent -import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -18,6 +31,7 @@ internal class WalletBackupModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() @@ -26,29 +40,84 @@ internal class WalletBackupModel @Inject constructor( field = MutableStateFlow( WalletBackupUM( onBackClick = { router.pop() }, - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.ComingSoon, - onRecoveryPhraseClick = { }, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), + onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, + backedUp = false, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + closeBs() + } + } + } + init { getWalletUseCase.invokeFlow(params.userWalletId) .map { it.getOrNull() } .distinctUntilChanged() + .filterNotNull() .onEach { - updateBackupStatuses() + updateBackupStatuses(it) } .launchIn(modelScope) } - private fun updateBackupStatuses() { + private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> - currentState.copy( - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.ComingSoon, + if (userWallet is UserWallet.Hot) { + currentState.updateBackupStatusesHotWallet(userWallet) + } else { + currentState + } + } + } + + private fun WalletBackupUM.updateBackupStatusesHotWallet(userWallet: UserWallet.Hot): WalletBackupUM = copy( + recoveryPhraseStatus = if (userWallet.backedUp) { + LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, ) + } else { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + }, + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), + backedUp = userWallet.backedUp, + ) + + private fun onRecoveryPhraseClick() { + if (uiState.value.backedUp) { + // TODO [REDACTED_TASK_KEY] + } else { + uiMessageSender.send(makeBackupAtFirstAlertBS) } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 7d5ac986a4..f5d25b7280 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -1,20 +1,14 @@ package com.tangem.features.hotwallet.walletbackup.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -28,7 +22,10 @@ import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.features.hotwallet.common.ui.OptionBlock import com.tangem.core.ui.R -import com.tangem.features.hotwallet.common.ui.DISABLED_COLORS_ALPHA +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.resourceReference @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -54,7 +51,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_seed_title), description = stringResourceSafe(R.string.hw_backup_seed_description), badge = { - BackupStatusBadge(status = state.recoveryPhraseStatus) + state.recoveryPhraseStatus?.let { Label(it) } }, onClick = state.onRecoveryPhraseClick, enabled = true, @@ -66,7 +63,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_google_drive_title), description = stringResourceSafe(R.string.hw_backup_google_drive_description), badge = { - BackupStatusBadge(status = state.googleDriveStatus) + state.googleDriveStatus?.let { Label(it) } }, onClick = state.onGoogleDriveClick, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, @@ -76,49 +73,6 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod } } -@Composable -private fun BackupStatusBadge(status: BackupStatus, modifier: Modifier = Modifier) { - val text = when (status) { - BackupStatus.Done -> stringResourceSafe(R.string.common_done) - BackupStatus.ComingSoon -> stringResourceSafe(R.string.common_coming_soon) - BackupStatus.NoBackup -> stringResourceSafe(R.string.hw_backup_no_backup) - } - - val backgroundColor by animateColorAsState( - targetValue = when (status) { - BackupStatus.Done -> TangemTheme.colors.text.accent.copy(alpha = 0.1f) - BackupStatus.ComingSoon -> TangemTheme.colors.control.unchecked.copy(DISABLED_COLORS_ALPHA) - BackupStatus.NoBackup -> TangemTheme.colors.text.warning.copy(alpha = 0.1f) - }, - ) - - val textColor by animateColorAsState( - targetValue = when (status) { - BackupStatus.Done -> TangemTheme.colors.text.accent - BackupStatus.ComingSoon -> TangemTheme.colors.text.secondary.copy(DISABLED_COLORS_ALPHA) - BackupStatus.NoBackup -> TangemTheme.colors.text.warning - }, - ) - - AnimatedContent(targetState = text) { text -> - Box( - modifier = modifier - .padding(horizontal = 4.dp) - .background( - color = backgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - ) { - Text( - text = text, - style = TangemTheme.typography.caption1, - color = textColor, - ) - } - } -} - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -131,25 +85,46 @@ private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider: private class WalletBackupUMProvider : CollectionPreviewParameterProvider( collection = listOf( WalletBackupUM( - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.ComingSoon, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, + backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.NoBackup, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, + backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = BackupStatus.Done, - googleDriveStatus = BackupStatus.Done, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ), onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, + backedUp = false, ), ), ) \ No newline at end of file diff --git a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt index 6a18888000..de05f5dc62 100644 --- a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt +++ b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt @@ -4,9 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext interface KycComponent { - fun launch() + fun launch(params: Params) interface Factory { fun create(appComponentContext: AppComponentContext): KycComponent } + + data class Params( + val targetAddress: String, + val cardId: String, + ) } \ No newline at end of file diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts index 3e07cc6bd5..1bf8fc66f2 100644 --- a/features/kyc/impl/build.gradle.kts +++ b/features/kyc/impl/build.gradle.kts @@ -13,8 +13,7 @@ android { dependencies { /** Api */ - //TODO disable for release because of the permissions - // implementation(projects.features.kyc.api) + implementation(projects.features.kyc.api) /** Domain */ implementation(projects.domain.visa) diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt index afabc4e184..92d39cee70 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -1,57 +1,41 @@ package com.tangem.features.kyc import com.sumsub.sns.core.SNSMobileSDK -import com.sumsub.sns.core.data.listener.SNSCompleteHandler import com.sumsub.sns.core.data.listener.TokenExpirationHandler -import com.sumsub.sns.core.data.model.SNSCompletionResult -import com.sumsub.sns.core.data.model.SNSInitConfig -import com.sumsub.sns.core.data.model.SNSSDKState import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.pay.repository.KycRepository -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.kyc.theme.TangemSNSTheme +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.kyc.theme.TangemSNSIconHandler +import com.tangem.features.kyc.theme.TangemSNSTheme import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.Locale class DefaultKycComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - private val kycRepositoryFactory: KycRepository.Factory, ) : KycComponent, AppComponentContext by appComponentContext { - private val kycRepository = kycRepositoryFactory.create(UserWalletId("0FFFFF")) + private val model: DefaultKycModel = getOrCreateModel() - override fun launch() { + override fun launch(params: KycComponent.Params) { componentScope.launch { - val startInfo = kycRepository.getKycStartInfo().getOrNull() ?: return@launch - - val tokenExpirationHandler = object : TokenExpirationHandler { - override fun onTokenExpired(): String? { - val newToken = runBlocking { kycRepository.getKycStartInfo().getOrNull()?.token } - return newToken + model.uiState.collect { + it?.let { startInfo -> + val tokenExpirationHandler = object : TokenExpirationHandler { + override fun onTokenExpired() = "" + } + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) + .withTheme(TangemSNSTheme.theme(activity)) + .withIconHandler(TangemSNSIconHandler()) + .withLocale(Locale("en")) + .build() + snsSdk.launch() } } - - val snsSdk = SNSMobileSDK.Builder(activity) - .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) - .withConf(SNSInitConfig(strings = mapOf())) - .withTheme(TangemSNSTheme.theme(activity)) - .withIconHandler(TangemSNSIconHandler()) - .withLocale(Locale("en")) - .withCompleteHandler( - object : SNSCompleteHandler { - override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) { - } - }, - ) - .build() - - snsSdk.launch() } + model.getKycToken(params) } @AssistedFactory diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt new file mode 100644 index 0000000000..aff81ad334 --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -0,0 +1,32 @@ +package com.tangem.features.kyc + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +class DefaultKycModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + kycRepositoryFactory: KycRepository.Factory, +) : Model() { + + private val kycRepository = kycRepositoryFactory.create() + + private val _uiState: MutableStateFlow = MutableStateFlow(null) + val uiState = _uiState.asStateFlow() + + fun getKycToken(params: KycComponent.Params) { + modelScope.launch { + kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull() + ?.let { _uiState.emit(it) } + } + } +} \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt index c72f282775..a2fefafc7b 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt @@ -1,11 +1,16 @@ package com.tangem.features.kyc.di +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model import com.tangem.features.kyc.DefaultKycComponent +import com.tangem.features.kyc.DefaultKycModel import com.tangem.features.kyc.KycComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module @InstallIn(SingletonComponent::class) @@ -13,4 +18,13 @@ internal interface FeatureModule { @Binds fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(DefaultKycModel::class) + fun provideModel(model: DefaultKycModel): Model } \ No newline at end of file diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index c3a7b45a50..b1eb0e3830 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /* Project - Core */ implementation(projects.core.ui) implementation(projects.core.decompose) + implementation(projects.core.analytics.models) /* Compose */ implementation(deps.compose.runtime) diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt index a12efc7654..b3616e31a9 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -12,11 +12,19 @@ interface ChooseManagedTokensComponent : ComposableContentComponent { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val source: Source, + val showSendViaSwapNotification: Boolean, + val callback: ModelCallback? = null, + val analyticsCategoryName: String, ) enum class Source { SendViaSwap, } + interface ModelCallback { + fun onResult() + fun onBack() + } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt new file mode 100644 index 0000000000..eba0758c21 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt @@ -0,0 +1,33 @@ +package com.tangem.features.managetokens.component.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.CHOSEN_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +sealed class CommonManageTokensAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + data class TokenSearchClicked( + val categoryName: String, + ) : CommonManageTokensAnalyticEvents(category = categoryName, event = "Token Search Clicked") + + /** Searched token chosen event */ + data class TokenSearched( + val categoryName: String, + val token: String?, + val blockchain: String?, + val isTokenChosen: Boolean, + ) : CommonManageTokensAnalyticEvents( + category = categoryName, + event = "Token Searched", + params = buildMap { + put(CHOSEN_TOKEN, if (isTokenChosen) "Yes" else "No") + token?.let { put(TOKEN_PARAM, token) } + blockchain?.let { put(BLOCKCHAIN, blockchain) } + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 3fcac8e481..0506848d40 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -30,8 +30,10 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.swap.models) + implementation(projects.domain.notifications) /* AndroidX */ implementation(deps.androidx.activity.compose) @@ -41,7 +43,6 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.foundation) - implementation(deps.compose.material) // For button colors implementation(deps.compose.material3) implementation(deps.compose.shimmer) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index 8e71b560bf..7b18b4dc84 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -59,9 +59,11 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( context = childByContext(componentContext), params = SwapChooseTokenNetworkComponent.Params( userWalletId = config.userWalletId, + analyticsCategoryName = params.analyticsCategoryName, initialCurrency = config.initialCurrency, selectedCurrency = config.selectedCurrency, token = config.token, + isSearchedToken = config.isSearchedToken, onDismiss = model.bottomSheetNavigation::dismiss, onResult = { swapCurrencies, cryptoCurrency -> componentScope.launch { @@ -70,7 +72,8 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( cryptoCurrency = cryptoCurrency, shouldResetNavigation = params.selectedCurrency != null, ) - router.pop() + model.bottomSheetNavigation.dismiss() + params.callback?.onResult() ?: router.pop() } }, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt index 77ee44682a..35d7732ad6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt @@ -14,5 +14,6 @@ internal sealed class ChooseManageTokensBottomSheetConfig { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val token: ManagedCryptoCurrency.Token, + val isSearchedToken: Boolean, ) : ChooseManageTokensBottomSheetConfig() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 83d65cc3a8..3c239162b1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -3,7 +3,9 @@ package com.tangem.features.managetokens.choosetoken.model import androidx.annotation.StringRes import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -15,11 +17,13 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.notifications.SetShouldShowNotificationUseCase import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent.Source import com.tangem.features.managetokens.component.ManageTokensSource +import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM @@ -38,11 +42,14 @@ import timber.log.Timber import javax.inject.Inject import kotlin.collections.isNotEmpty +@Suppress("LongParameterList") @ModelScoped internal class ChooseManagedTokensModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val uiMessageSender: UiMessageSender, + private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, manageTokensListManagerFactory: ManageTokensListManager.Factory, ) : Model() { @@ -57,6 +64,7 @@ internal class ChooseManagedTokensModel @Inject constructor( initialCurrency = params.initialCurrency, selectedCurrency = params.selectedCurrency, token = token, + isSearchedToken = uiState.value.readContent.search.isActive, ), ) }, @@ -86,13 +94,13 @@ internal class ChooseManagedTokensModel @Inject constructor( return ChooseManagedTokenUM( notificationUM = getNotification(), readContent = ManageTokensUM.ReadContent( - popBack = router::pop, + popBack = { params.callback?.onBack() ?: router.pop() }, isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_choose_token), - onBackButtonClick = router::pop, + onBackButtonClick = { params.callback?.onBack() ?: router.pop() }, ), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), @@ -107,18 +115,21 @@ internal class ChooseManagedTokensModel @Inject constructor( } private fun getNotification(): NotificationUM? { - return when (params.source) { - Source.SendViaSwap -> ChooseManagedTokensNotificationUM.SendViaSwap( - onCloseClick = ::removeNotification, - ) + return if (params.source == Source.SendViaSwap && params.showSendViaSwapNotification) { + ChooseManagedTokensNotificationUM.SendViaSwap(onCloseClick = ::removeNotification) + } else { + null } } private fun removeNotification() { - uiState.update { - it.copy( - notificationUM = null, - ) + modelScope.launch { + setShouldShowNotificationUseCase(NotificationId.SendViaSwapTokenSelectorNotification.key, false) + uiState.update { + it.copy( + notificationUM = null, + ) + } } } @@ -126,6 +137,17 @@ internal class ChooseManagedTokensModel @Inject constructor( private fun observeSearchQueryChanges() { uiState .distinctUntilChanged { old, new -> + if (!new.readContent.search.isActive && old.readContent.search.isActive) { + analyticsEventHandler.send( + CommonManageTokensAnalyticEvents.TokenSearched( + params.analyticsCategoryName, + token = null, + blockchain = null, + isTokenChosen = false, + ), + ) + } + // It's also used to skip search activation to avoid searching an empty query old.readContent.search.query == new.readContent.search.query && new.readContent.search.isActive } @@ -245,6 +267,11 @@ internal class ChooseManagedTokensModel @Inject constructor( } private fun toggleSearchBar(isActive: Boolean) { + if (isActive) { + analyticsEventHandler.send( + CommonManageTokensAnalyticEvents.TokenSearchClicked(params.analyticsCategoryName), + ) + } uiState.update { state -> @StringRes val placeholderTextRes = if (isActive) { R.string.manage_tokens_search_placeholder diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 80da5da487..eca3ff5cc5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -65,6 +65,7 @@ internal class PreviewCustomTokenSelectorComponent( hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "Network $index", type = "N$index", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 55ac0bba8f..d7caaf672c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -42,9 +42,9 @@ internal class PreviewManageTokensComponent( ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = {}, - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_plus_24, - onIconClicked = {}, + onClicked = {}, ), ) } else { @@ -163,6 +163,7 @@ internal class PreviewManageTokensComponent( hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "NETWORK$networkIndex", type = "N$networkIndex", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt index bc1276188d..28da1876e7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt @@ -104,6 +104,7 @@ internal class PreviewOnboardingManageTokensComponent( hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "NETWORK$networkIndex", type = "N$networkIndex", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index fdcf5a9d1d..08d5f86574 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -9,12 +9,12 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.DialogMessage -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index ff768a5e76..91e1814599 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -17,9 +17,9 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent @@ -129,9 +129,9 @@ internal class ManageTokensModel @Inject constructor( topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_plus_24, - onIconClicked = ::navigateToAddCustomToken, + onClicked = ::navigateToAddCustomToken, ), ), search = SearchBarUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index d6b6ca0899..30f64dad46 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -13,10 +13,10 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.redux.OnboardingManageTokensAction import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.component.OnboardingManageTokensComponent diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 6029226493..df2755faef 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 5e4e7a4d4d..0778b806c6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -18,9 +18,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -164,11 +165,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( type = percentChangeType.toChartType(), xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), yAxisFormatter = { value -> - BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = value, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ) + value.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + } }, ) } @@ -196,11 +198,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( val state = MutableStateFlow( MarketsTokenDetailsUM( tokenName = params.token.name, - priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = params.token.tokenQuotes.currentPrice, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + priceText = params.token.tokenQuotes.currentPrice.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, dateTimeText = resourceReference(R.string.common_today), priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), @@ -403,7 +406,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( state.update { it.copy( - priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceText = newInfo.quotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + }, priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( interval = it.selectedInterval, ), @@ -490,7 +498,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: getDefaultDateTimeString(currentState.selectedInterval) - val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value) + val priceText = (price ?: currentQuotes.value.currentPrice).format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + } val percent = price?.let { getChangePercentBetween( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt index 0ddafb4e83..eb55abd9dd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt @@ -5,7 +5,9 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.markets.TokenMarketExchange import com.tangem.domain.markets.TokenMarketExchange.TrustScore import com.tangem.features.markets.impl.R @@ -29,11 +31,12 @@ internal object ExchangeItemStateConverter : Converter h24ChangePercent @@ -66,8 +57,8 @@ internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: Bi } internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = currentPrice).first - val updated = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = updatedPrice).first + val current = getFiatPriceAmountWithScale(value = currentPrice).first + val updated = getFiatPriceAmountWithScale(value = updatedPrice).first return when { updated > current -> PriceChangeType.UP diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt index 4bf8b58e27..74d10c5742 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -3,6 +3,9 @@ package com.tangem.features.markets.details.impl.model.state import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo @@ -57,7 +60,12 @@ internal class QuotesStateUpdater( state.update { stateToUpdate -> stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceText = newQuotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency().symbol, + fiatCurrencyCode = currentAppCurrency().code, + ).price() + }, priceChangePercentText = newQuotes.getFormattedPercentByInterval( interval = stateToUpdate.selectedInterval, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt index 253d74b0ce..ac34715c4d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt @@ -3,10 +3,10 @@ package com.tangem.features.markets.portfolio.impl.loader import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState /** diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index deb96eacdb..e17c925595 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.HasMissedDerivationsUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index caa873be75..5c5bb59b96 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -3,7 +3,7 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.ArtworkModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 8601ed1193..e1acbcf4e1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -4,10 +4,10 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index 85478242c5..9e5aba6a0a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -2,7 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 372309d5ea..bb1496cfe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -11,9 +11,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetCurrencyQuotesUseCase @@ -86,13 +87,14 @@ internal class TokenMarketBlockModel @Inject constructor( ) state.value = state.value.copy( - currentPrice = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = res.fiatRate, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencyCode = currentAppCurrency.value.code, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + currentPrice = res.fiatRate.format { + fiat( + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencyCode = currentAppCurrency.value.code, + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index d7352826b5..22712a972c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -7,11 +7,7 @@ import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.impl.R @@ -94,11 +90,12 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotesShort?.currentPrice - val priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = tokenQuotesShort.currentPrice, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } val changeType = if (prevPrice != null) { if (tokenQuotesShort.currentPrice > prevPrice) { diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt index ed1df0a1f2..801c8f6420 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt @@ -2,8 +2,9 @@ package com.tangem.features.nft.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.nft.models.NFTAsset +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.nft.models.NFTAsset interface NFTDetailsBlockComponent : ComposableContentComponent { @@ -11,6 +12,8 @@ interface NFTDetailsBlockComponent : ComposableContentComponent { val userWalletId: UserWalletId, val nftAsset: NFTAsset, val nftCollectionName: String, + val title: TextReference, + val isSuccessScreen: Boolean, ) interface Factory : ComponentFactory diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 83bba9f0b5..0e4b9f5c67 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt index 234cb0adfb..4856ca5634 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt @@ -22,6 +22,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor( assetName = stringReference(params.nftAsset.name.orEmpty()), collectionName = stringReference(params.nftCollectionName), assetImage = params.nftAsset.media?.imageUrl, + title = params.title, + isSuccessScreen = params.isSuccessScreen, networkIconRes = getActiveIconRes(params.nftAsset.network.rawId), ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt index 6336d2dbb2..8d7d1148c9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -19,12 +20,15 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.common.ui.NFTLogo import com.tangem.features.nft.impl.R +@Suppress("LongParameterList") @Composable internal fun NFTDetailsBlock( + title: TextReference, assetName: TextReference, collectionName: TextReference, assetImage: String?, networkIconRes: Int, + isSuccessScreen: Boolean, ) { Column( modifier = Modifier @@ -35,20 +39,21 @@ internal fun NFTDetailsBlock( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = "NFT Asset", + text = title.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - NFTLogo( - assetImage, - networkIconRes, - background = TangemTheme.colors.background.action, - ) - + if (isSuccessScreen) { + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { @@ -63,6 +68,14 @@ internal fun NFTDetailsBlock( color = TangemTheme.colors.text.tertiary, ) } + if (!isSuccessScreen) { + SpacerWMax() + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } } } } @@ -78,6 +91,8 @@ private fun NFTDetailsBlock_Preview() { collectionName = stringReference("NFT Collection"), assetImage = null, networkIconRes = R.drawable.img_polygon_22, + title = stringReference("From My Wallet"), + isSuccessScreen = false, ) } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index 2c3ad2ede5..45319ee1ba 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -28,9 +28,8 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { ) { TangemTopAppBar( modifier = Modifier, - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.onBackClick, ), title = state.nftAsset.name, ) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt index 465271494a..2a156d878d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt index 21c147c3a9..dd9cfc1aa7 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -28,9 +28,8 @@ internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { ) { TangemTopAppBar( modifier = Modifier, - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = state.onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.onBackClick, ), title = stringResourceSafe(id = R.string.nft_receive_title), subtitle = state.appBarSubtitle.resolveReference(), diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt index 9f18df108f..f1ebd94f83 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt @@ -22,9 +22,8 @@ internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifi ) { TangemTopAppBar( modifier = Modifier, - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.onBackClick, ), title = stringResourceSafe(R.string.nft_traits_title), ) diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index eac8270d6f..9653f895cb 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -62,7 +62,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index ac205fd637..24dc8aaa95 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -206,7 +206,7 @@ internal class OnboardingEntryModel @Inject constructor( router.replaceAll(AppRoute.Wallet) } } else { - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Home()) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 23b2dbb674..0c7b8d6290 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent @@ -51,6 +52,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, private val walletsRepository: WalletsRepository, @@ -231,7 +233,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( OnboardingMultiWalletComponent.Mode.Onboarding, OnboardingMultiWalletComponent.Mode.ContinueFinalize, -> { - userWalletsListManager.save( + saveWalletUseCase( userWallet = userWalletCreated.copy( scanResponse = scanResponse.updateScanResponseAfterBackup(), ), @@ -247,13 +249,11 @@ internal class MultiWalletFinalizeModel @Inject constructor( } ?: userWalletCreated - userWalletsListManager.update( - userWalletId = userWallet.walletId, - update = { wallet -> - wallet.requireColdWallet().copy( - scanResponse = scanResponse.updateScanResponseAfterBackup(), - ) - }, + saveWalletUseCase( + userWallet = userWallet.requireColdWallet().copy( + scanResponse = scanResponse.updateScanResponseAfterBackup(), + ), + canOverride = true, ) userWallet diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index bea41ef278..7f15990f3c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -20,10 +20,10 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, + val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { private val model: OnboardingNoteModel = getOrCreateModel(params) @@ -98,14 +99,14 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( childParams = childParams, onWalletCreated = { userWallet -> model.onWalletCreated(userWallet) - model.stackNavigation.push(OnboardingNoteRoute.TopUp) + model.stackNavigation.push(OnboardingNoteRoute.Done) }, ), ) - OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent( - appComponentContext = factoryContext, - params = OnboardingNoteTopUpComponent.Params( - childParams = childParams, + OnboardingNoteRoute.Done -> onboardingDoneComponentFactory.create( + context = factoryContext, + params = OnboardingDoneComponent.Params( + mode = OnboardingDoneComponent.Mode.WalletCreated, onDone = { params.onDone() }, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt deleted file mode 100644 index 5b9cc4f1af..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp - -internal class OnboardingNoteTopUpComponent( - appComponentContext: AppComponentContext, - private val params: Params, -) : ComposableContentComponent, AppComponentContext by appComponentContext { - - private val model: OnboardingNoteTopUpModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - - BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) - - OnboardingNoteTopUp( - modifier = modifier, - state = state, - ) - } - - data class Params( - val childParams: DefaultOnboardingNoteComponent.ChildParams, - val onDone: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt deleted file mode 100644 index 01f73e63a2..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.model - -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.extensions.isPositive -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList") -@ModelScoped -internal class OnboardingNoteTopUpModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val rampStateManager: RampStateManager, - private val cardRepository: CardRepository, - private val saveWalletUseCase: SaveWalletUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, -) : Model() { - - private val params = paramsContainer.require() - private val commonState = params.childParams.commonState - private val scanResponse = params.childParams.commonState.value.scanResponse - private var userWallet = params.childParams.commonState.value.userWallet - - private val _uiState = MutableStateFlow( - OnboardingNoteTopUpUM( - onRefreshBalanceClick = ::refreshBalance, - onBuyCryptoClick = ::onBuyCryptoClick, - onShowWalletAddressClick = ::onShowWalletAddressClick, - onDismissBottomSheet = ::onDismissBottomSheet, - ), - ) - - val uiState: StateFlow = _uiState - - init { - Analytics.send(OnboardingEvent.Topup.ScreenOpened) - observeArtwork() - modelScope.launch { - createUserWalletIfNull() - cardRepository.finishCardActivation(scanResponse.card.cardId) - observeCryptoCurrencyStatus() - refreshBalance() - } - } - - private fun refreshBalance() { - modelScope.launch { - showBalanceLoadingProgress(true) - createUserWalletIfNull() - val userWalletId = requireNotNull(userWallet?.walletId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true) - } - showBalanceLoadingProgress(false) - } - } - - private fun onBuyCryptoClick() { - val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - modelScope.launch { - getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight { - urlOpener.openUrl(it) - } - } - Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency)) - } - - private fun onShowWalletAddressClick() { - val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - val networkAddress = currencyStatus.value.networkAddress ?: return - - _uiState.update { - it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) - } - Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) - } - - private fun onDismissBottomSheet() { - _uiState.update { - it.copy(addressBottomSheetConfig = null) - } - } - - private suspend fun createUserWalletIfNull() { - if (userWallet != null) { - return - } - val commonState = params.childParams.commonState.value - userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse) - } - - private fun observeArtwork() { - modelScope.launch { - params.childParams.commonState.collect { - _uiState.value = _uiState.value.copy( - cardArtwork = it.cardArtwork, - ) - } - } - } - - private fun observeCryptoCurrencyStatus() { - val userWalletId = userWallet?.walletId ?: return - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) - .map { it.getOrNull() } - .filterNotNull() - .onEach(::applyCryptoCurrencyStatusToState) - .launchIn(modelScope) - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - if (commonState.value.cryptoCurrencyStatus == null) { - loadAvailableForBuy(status) - } - - commonState.update { - it.copy(cryptoCurrencyStatus = status) - } - - val amount = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.amount - is CryptoCurrencyStatus.NoAccount -> status.value.amount - is CryptoCurrencyStatus.NoQuote -> status.value.amount - else -> null - } - val hasCurrentNetworkTransactions = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions - is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions - else -> false - } - val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount - - if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { - params.onDone() - } - - _uiState.update { - it.copy( - amountToCreateAccount = amountToCreateAccount - ?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }, - balance = amount?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }.orEmpty(), - isTopUpDataLoading = status.value.networkAddress == null, - ) - } - } - - private fun showBalanceLoadingProgress(value: Boolean) { - _uiState.update { - it.copy(isRefreshing = value) - } - } - - private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { - modelScope.launch { - val availableForBuy = rampStateManager.availableForBuy( - userWallet = userWallet ?: return@launch, - cryptoCurrency = cryptoCurrencyStatus.currency, - ) - _uiState.update { - it.copy( - availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None, - availableForBuyLoading = false, - ) - } - } - } - - private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) = - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = uiState.value.onDismissBottomSheet, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currencyStatus.currency.name, - symbol = currencyStatus.currency.symbol, - ), - network = currencyStatus.currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ) - - private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet { - val wallet = requireNotNull( - value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(), - lazyMessage = { "User wallet not created" }, - ) - saveWalletUseCase(wallet, false) - return wallet - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt deleted file mode 100644 index 672d5e838b..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -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.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.LocalTangemShimmer -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.common.ui.RefreshButton -import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R -import com.valentinilk.shimmer.shimmer - -@Composable -fun OnboardingNoteTopUpHeader( - balance: String, - cardArtwork: ArtworkUM?, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .heightIn(min = 180.dp) - .widthIn(max = 450.dp), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 32.dp), - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - modifier = if (balance.isEmpty()) { - Modifier - .width(120.dp) - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) - .shimmer(LocalTangemShimmer.current) - } else { - Modifier - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - text = balance, - ) - SpacerHMax() - } - } - WalletCard( - modifier = Modifier.width(120.dp).align(Alignment.TopCenter), - artwork = cardArtwork, - ) - RefreshButton( - modifier = Modifier.align(Alignment.BottomCenter), - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardinNoteTopUpHeaderPreview() { - TangemThemePreview { - OnboardingNoteTopUpHeader( - balance = "0.00000001 BTC", - cardArtwork = ArtworkUM(null, ""), - onRefreshBalanceClick = {}, - isRefreshing = false, - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt deleted file mode 100644 index 476d06b378..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM - -@Composable -fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnboardingNoteTopUpHeader( - balance = state.balance, - cardArtwork = state.cardArtwork, - onRefreshBalanceClick = state.onRefreshBalanceClick, - isRefreshing = state.isRefreshing, - modifier = Modifier - .padding(top = 64.dp) - .padding(horizontal = 24.dp) - .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth(), - ) - Column( - modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 16.dp), - ) - - val text = if (state.amountToCreateAccount != null) { - stringResourceSafe( - R.string.onboarding_top_up_min_create_account_amount, - state.amountToCreateAccount, - ) - } else { - stringResourceSafe(R.string.onboarding_top_up_body) - } - SpacerH16() - Text( - text = text, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - - BottomButtons(state) - - state.addressBottomSheetConfig?.let { config -> - TokenReceiveBottomSheet(config = config) - } - } -} - -@Composable -private fun BottomButtons(state: OnboardingNoteTopUpUM) { - if (state.availableForBuy) { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - } else { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_button_receive_crypto), - onClick = state.onShowWalletAddressClick, - ) - } - AnimatedVisibility(visible = !state.availableForBuyLoading) { - if (state.availableForBuy) { - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowWalletAddressClick, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardingNoteTopUpPreview() { - TangemThemePreview { - OnboardingNoteTopUp( - state = OnboardingNoteTopUpUM( - availableForBuy = true, - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt deleted file mode 100644 index c9d9ca3541..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig - -data class OnboardingNoteTopUpUM( - val cardArtwork: ArtworkUM? = null, - val availableForBuy: Boolean = false, - val availableForBuyLoading: Boolean = true, - val balance: String = "", - val isRefreshing: Boolean = false, - val isTopUpDataLoading: Boolean = true, - val amountToCreateAccount: String? = null, - val addressBottomSheetConfig: TangemBottomSheetConfig? = null, - val onBuyCryptoClick: () -> Unit = {}, - val onShowWalletAddressClick: () -> Unit = {}, - val onRefreshBalanceClick: () -> Unit = {}, - val onDismissBottomSheet: () -> Unit = {}, -) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt index be064600cc..136a2ae24d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import dagger.Binds import dagger.Module @@ -37,9 +36,4 @@ internal interface ModelModule { @IntoMap @ClassKey(OnboardingNoteCreateWalletModel::class) fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model - - @Binds - @IntoMap - @ClassKey(OnboardingNoteTopUpModel::class) - fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt index c1cd153231..704923459b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -1,8 +1,8 @@ package com.tangem.features.onboarding.v2.note.impl.model import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet internal data class OnboardingNoteCommonState( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 5d165cbe97..ae47a8ab9d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -84,7 +84,7 @@ internal class OnboardingNoteModel @Inject constructor( return if (card.wallets.isEmpty()) { OnboardingNoteRoute.CreateWallet } else { - OnboardingNoteRoute.TopUp + OnboardingNoteRoute.Done } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt index 1d7573e677..cc8101af19 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt @@ -9,7 +9,7 @@ internal sealed class OnboardingNoteRoute { data object CreateWallet : OnboardingNoteRoute() @Serializable - data object TopUp : OnboardingNoteRoute() + data object Done : OnboardingNoteRoute() } -internal const val ONBOARDING_NOTE_STEPS_COUNT = 3 \ No newline at end of file +internal const val ONBOARDING_NOTE_STEPS_COUNT = 2 \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt index 3df643e22b..83b41e6d8f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt @@ -2,5 +2,5 @@ package com.tangem.features.onboarding.v2.note.impl.route internal fun OnboardingNoteRoute.stepNum() = when (this) { OnboardingNoteRoute.CreateWallet -> 1 - OnboardingNoteRoute.TopUp -> 2 + OnboardingNoteRoute.Done -> 2 } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt index 50136e9d10..e3e01fe44d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt @@ -49,7 +49,7 @@ internal fun OnboardingStepper( ) { TangemTopAppBar( startButton = TopAppBarButtonUM.Back(onBackClick), - endButton = TopAppBarButtonUM(iconRes = R.drawable.ic_chat_24, onIconClicked = onSupportButtonClick) + endButton = TopAppBarButtonUM.Icon(iconRes = R.drawable.ic_chat_24, onClicked = onSupportButtonClick) .takeIf { state.steps != state.currentStep }, title = if (state.steps == state.currentStep) { resourceReference(R.string.common_done) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index fa3a235361..ab86ceed99 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -7,43 +7,26 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.datasource.local.config.issuers.IssuersConfigStorage -import com.tangem.domain.card.common.util.twinsIsTwinned import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.impl.R @@ -60,11 +43,9 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -73,22 +54,15 @@ internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, private val tangemSdkManager: TangemSdkManager, private val issuersConfigStorage: IssuersConfigStorage, private val cardRepository: CardRepository, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val tokensFeatureToggles: TokensFeatureToggles, - private val walletBalanceFetcher: WalletBalanceFetcher, ) : Model() { private val params = paramsContainer.require() @@ -114,14 +88,10 @@ internal class OnboardingTwinModel @Inject constructor( ) } Mode.CreateWallet -> { - if (params.scanResponse.twinsIsTwinned()) { - OnboardingTwinUM.TopUpPrepare - } else { - OnboardingTwinUM.Welcome( - pairCardNumber = firstCardTwinNumber.pairNumber().number, - onContinueClick = ::navigateToFirstScan, - ) - } + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) } }, ) @@ -137,11 +107,6 @@ internal class OnboardingTwinModel @Inject constructor( saveTwinsOnboardingShownUseCase() } } - OnboardingTwinUM.TopUpPrepare -> { - modelScope.launch { - setTopUpState(params.scanResponse) - } - } else -> {} } } @@ -199,9 +164,9 @@ internal class OnboardingTwinModel @Inject constructor( // remove wallet only after first step of retwin if (params.mode == Mode.RecreateWallet) { - userWalletsListManager.delete( - listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), - ) + UserWalletIdBuilder.scanResponse(params.scanResponse).build()?.let { + deleteWalletUseCase(it) + } } analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) @@ -216,10 +181,7 @@ internal class OnboardingTwinModel @Inject constructor( }, ) } - - innerNavigationState.update { - it.copy(stackSize = 2) - } + innerNavigationState.update { it.copy(stackSize = 2) } } } } @@ -227,7 +189,6 @@ internal class OnboardingTwinModel @Inject constructor( private fun createSecondWallet(firstPublicKey: String) { setLoading(true) - modelScope.launch { val secondCardNumber = firstCardTwinNumber.pairNumber().number val result = tangemSdkManager.createSecondTwinWallet( @@ -316,134 +277,31 @@ internal class OnboardingTwinModel @Inject constructor( Mode.CreateWallet -> { modelScope.launch { setLoading(true) - setTopUpState(scanResponse) + finishActivation(scanResponse) }.saveIn(cryptoCurrencyStatusJobHolder) } } } - private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope { + private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope { val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run { Timber.e("User wallet not created") setLoading(false) return@coroutineScope } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@coroutineScope + } cardRepository.finishCardActivation(params.scanResponse.card.cardId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - } else { - fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true) - } - .onLeft { - Timber.e("Unable to fetch currency status: $it") - setLoading(false) - } - - val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .firstOrNull()?.getOrNull() - ?: run { - setLoading(false) - Timber.e("Unable to get currency status") - return@coroutineScope - } - - launch { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .collect { - it.onRight { status -> - applyCryptoCurrencyStatusToState(status) - } - } - } - - _uiState.value = OnboardingTwinUM.TopUp( - onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, - onRefreshClick = { onRefreshBalanceClick(userWallet) }, - onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, - isLoading = true, - ) - - innerNavigationState.update { - it.copy(stackSize = 4) - } - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return - if (amount > BigDecimal.ZERO) { - params.modelCallbacks.onDone() - } else { - update { - it.copy( - balance = BigDecimal.ZERO.format { crypto(status.currency) }, - onBuyCryptoClick = { onBuyCryptoClick(status) }, - onShowAddressClick = { onShowAddressClick(status) }, - isLoading = false, - ) - } - } - } - - private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { - modelScope.launch { - getLegacyTopUpUrlUseCase(status).onRight { - urlOpener.openUrl(it) - } - } - } - - private fun onShowAddressClick(status: CryptoCurrencyStatus) { - val currency = status.currency - val networkAddress = status.value.networkAddress ?: return - - update { - it.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = { - update { - it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) - } - }, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currency.name, - symbol = currency.symbol, - ), - network = currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ), - ) - } - } - - private fun onRefreshBalanceClick(userWallet: UserWallet) { - update { - it.copy(isLoading = true) - } - modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true) - } - } + params.modelCallbacks.onDone() } private fun saveWalletAndDone() { @@ -456,7 +314,15 @@ internal class OnboardingTwinModel @Inject constructor( return@launch } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@launch + } + params.modelCallbacks.onDone() } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index c67669148b..d2e579d459 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH16 -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme @@ -43,11 +41,6 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi .weight(.48f) .fillMaxWidth(), state = state.artwork, - balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "", - isRefreshing = state.isLoading, - onRefreshBalanceClick = { - (state as? OnboardingTwinUM.TopUp)?.onRefreshClick() - }, ) AnimatedContent( @@ -60,16 +53,10 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi when (st) { is OnboardingTwinUM.ResetWarning -> ResetWarning(st) is OnboardingTwinUM.ScanCard -> ScanCard(st) - is OnboardingTwinUM.TopUp -> TopUp(st) is OnboardingTwinUM.Welcome -> Welcome(st) - OnboardingTwinUM.TopUpPrepare -> {} } } } - - if (state is OnboardingTwinUM.TopUp) { - TokenReceiveBottomSheet(config = state.bottomSheetConfig) - } } @Suppress("LongMethod") @@ -154,55 +141,6 @@ private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifie } } -@Composable -private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Column( - modifier = Modifier - .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - SpacerH16() - - Text( - text = stringResourceSafe(R.string.onboarding_top_up_body), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body1, - ) - } - - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowAddressClick, - ) - } -} - @Composable private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) { Column( @@ -287,14 +225,6 @@ private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifi } } -@Preview(showBackground = true) -@Composable -private fun PreviewTopUp() { - TangemThemePreview { - OnboardingTwin(OnboardingTwinUM.TopUp()) - } -} - @Preview(showBackground = true) @Composable private fun PreviewWelcome() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index 97758be4e1..3e0240af22 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -1,12 +1,10 @@ package com.tangem.features.onboarding.v2.twin.impl.ui +import android.annotation.SuppressLint import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -16,21 +14,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.zIndex -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.wallets.models.Artwork -import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit @@ -45,8 +37,6 @@ internal sealed class TwinWalletArtworkUM { FirstCard, SecondCard } } - - data object TopUp : TwinWalletArtworkUM() } private data class CardsTransitionState( @@ -64,15 +54,10 @@ private data class WalletCardTransitionState( val zIndex: Float = 0f, ) +@SuppressLint("UnusedBoxWithConstraintsScope") @Suppress("LongMethod") @Composable -internal fun TwinWalletArtworks( - state: TwinWalletArtworkUM, - balance: String, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun TwinWalletArtworks(state: TwinWalletArtworkUM, modifier: Modifier = Modifier) { BoxWithConstraints( modifier .heightIn(min = 180.dp) @@ -110,22 +95,6 @@ internal fun TwinWalletArtworks( } } - AnimatedVisibility( - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - ) - } - AnimatedTwinCards( transition1 = transition1, transition2 = transition2, @@ -133,46 +102,6 @@ internal fun TwinWalletArtworks( .widthIn(max = 450.dp) .matchParentSize(), ) - - AnimatedVisibility( - modifier = Modifier.align(Alignment.Center), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - text = balance, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - } - - AnimatedVisibility( - modifier = Modifier.align(Alignment.BottomCenter), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - RefreshButton( - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } } } @@ -308,26 +237,6 @@ private fun TwinWalletArtworkUM.toTransitionSetState( ) } } - TwinWalletArtworkUM.TopUp -> { - val scale = 0.4f - val yTranslation = -maxHeightDp * density - 24 * density - listOf( - CardsTransitionState( - walletCard1 = WalletCardTransitionState( - yTranslation = yTranslation, - xScale = scale, - yScale = scale, - zIndex = 2f, - ), - walletCard2 = WalletCardTransitionState( - yTranslation = yTranslation * 0.35f, - xScale = scale * 0.8f, - yScale = scale * 0.8f, - zIndex = 1f, - ), - ), - ) - } } @Preview(showBackground = true, widthDp = 360, heightDp = 640) @@ -341,16 +250,13 @@ private fun Preview() { .fillMaxSize(), contentAlignment = Alignment.Center, ) { - var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.Spread) } TwinWalletArtworks( state = state, modifier = Modifier .padding(top = 250.dp) .fillMaxWidth(), - balance = "1 USD", - isRefreshing = false, - onRefreshBalanceClick = {}, ) var index by remember { mutableIntStateOf(0) } @@ -366,7 +272,6 @@ private fun Preview() { TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), - TwinWalletArtworkUM.TopUp, ) state = list[index % list.size] diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt index 7341af8545..defa7831a4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.onboarding.v2.twin.impl.ui.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM @Immutable @@ -11,12 +10,6 @@ internal sealed class OnboardingTwinUM { abstract val isLoading: Boolean abstract val artwork: TwinWalletArtworkUM - data object TopUpPrepare : OnboardingTwinUM() { - override val stepIndex: Int = 0 - override val isLoading: Boolean = false - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread - } - data class Welcome( override val isLoading: Boolean = false, val pairCardNumber: Int = 2, @@ -56,23 +49,9 @@ internal sealed class OnboardingTwinUM { override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) } - data class TopUp( - override val isLoading: Boolean = false, - val balance: String = "", - val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, - val onBuyCryptoClick: () -> Unit = {}, - val onShowAddressClick: () -> Unit = {}, - val onRefreshClick: () -> Unit = {}, - ) : OnboardingTwinUM() { - override val stepIndex: Int = 2 - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp - } - fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { is Welcome -> copy(isLoading = isLoading) is ResetWarning -> copy() is ScanCard -> copy(isLoading = isLoading) - is TopUp -> copy(isLoading = isLoading) - TopUpPrepare -> this } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt index 5cd9a11f10..dff0ca06c5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -1,15 +1,11 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -19,9 +15,9 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.outsetBorder import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -112,17 +108,7 @@ private fun SelectableChainRow( RowContentContainer( modifier = modifier .heightIn(min = 48.dp) - .outsetBorder( - color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent, - width = 5.dp, - shape = RoundedCornerShape(size = 18.dp), - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .border( - width = 1.dp, - color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) + .selectedBorder(selected) .clickable(onClick = onClick) .padding(12.dp), icon = { @@ -163,7 +149,7 @@ private fun Preview() { ), ), selectedOption = SelectableChainRowUM( - event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet, + event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet, icon = R.drawable.ic_tangem_24, text = TextReference.Str("Tangem Wallet"), ), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 102098867b..e517a628cb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -20,8 +20,8 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent @@ -46,7 +46,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -173,7 +173,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } val userWallet = createUserWallet(params.scanResponse, newTokens) - userWalletsListManager.save(userWallet) + saveWalletUseCase(userWallet) visaAuthTokenStorage.remove(params.scanResponse.card.cardId) otpStorage.removeOTP(params.scanResponse.card.cardId) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt index 65fc1a9b0f..4de22cecd9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -215,7 +215,8 @@ internal class OnboardingVisaModel @Inject constructor( private fun tryToFindExistingWalletCardId(targetAddress: String): String? { val wallets = getWalletsUseCase.invokeSync().filter { it.isLocked.not() } - return wallets.filterIsInstance().firstOrNull { wallet -> // TODO [REDACTED_TASK_KEY] + // TODO [REDACTED_TASK_KEY] [Hot Wallet] Visa 1.0 flow. Hot wallet as a customer wallet + return wallets.filterIsInstance().firstOrNull { wallet -> wallet.scanResponse.card.wallets.any { val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index efcd01bc52..8280707bae 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -63,7 +63,6 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.shimmer) implementation(deps.compose.coil) - implementation(deps.compose.material) /** Other */ implementation(deps.decompose.ext.compose) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt index a416668d6f..0cdec24a2b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt @@ -2,7 +2,7 @@ package com.tangem.features.onramp.hottokens import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index 3f47acea4b..63b2225169 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -4,7 +4,7 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index 4015d007a8..ae9041847c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -24,14 +24,13 @@ internal sealed interface OnrampMainComponentUM { ) : OnrampMainComponentUM { override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = onClose, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = onClose, enabled = true, ), - endButtonUM = TopAppBarButtonUM( + endButtonUM = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = openSettings, + onClicked = openSettings, enabled = false, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 0fbbcbe0ca..8eb611e8fe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency @@ -37,7 +38,10 @@ internal class OnrampStateFactory( fun getReadyState(currency: OnrampCurrency): OnrampMainComponentUM.Content { val state = currentStateProvider() - val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) + val endButton = when (val button = state.topBarConfig.endButtonUM) { + is TopAppBarButtonUM.Icon -> button.copy(enabled = true) + is TopAppBarButtonUM.Text -> button.copy(enabled = true) + } return OnrampMainComponentUM.Content( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), buyButtonConfig = state.buyButtonConfig, @@ -80,7 +84,10 @@ internal class OnrampStateFactory( fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM { val state = currentStateProvider() - val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) + val endButton = when (val button = state.topBarConfig.endButtonUM) { + is TopAppBarButtonUM.Icon -> button.copy(enabled = true) + is TopAppBarButtonUM.Text -> button.copy(enabled = true) + } return when (state) { is OnrampMainComponentUM.Content -> state.copy( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index 07d5b17e3c..a493e1a7c9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection @@ -26,6 +27,7 @@ import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM @@ -84,7 +86,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ) - .requiredHeightIn(min = TangemTheme.dimens.size32), + .requiredHeightIn(min = TangemTheme.dimens.size32) + .testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD), ) LaunchedEffect(key1 = Unit) { @@ -101,7 +104,8 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { top = TangemTheme.dimens.spacing8, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - ), + ) + .testTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT), contentAlignment = Alignment.Center, ) { when (state) { @@ -138,12 +142,15 @@ private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier AsyncImage( modifier = Modifier .size(TangemTheme.dimens.size40) - .clip(CircleShape), + .clip(CircleShape) + .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), model = currencyUM.iconUrl, contentDescription = null, ) Icon( - modifier = Modifier.size(TangemTheme.dimens.size16), + modifier = Modifier + .size(TangemTheme.dimens.size16) + .testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON), painter = painterResource(id = R.drawable.ic_chevron_24), tint = TangemTheme.colors.icon.informative, contentDescription = null, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt index d63b3aa736..fe7e8c0075 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampMainComponentUM import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM @@ -77,6 +79,7 @@ private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ), + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK), onClick = { offset -> clickableAnnotation.getStringAnnotations( tag = TERMS_OF_USE_KEY, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt index e13fd96310..3693715b68 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -13,12 +13,14 @@ 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.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import com.tangem.core.ui.extensions.appendSpace import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon @@ -60,6 +62,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: }, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE), ) Text( text = buildAnnotatedString { @@ -69,6 +72,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: }, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT), ) } AnimatedVisibility( @@ -108,6 +112,7 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) { text = stringResourceSafe(id = R.string.express_provider), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE), ) Row( verticalAlignment = Alignment.CenterVertically, @@ -122,6 +127,7 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) { text = stringResourceSafe(id = R.string.express_fetch_best_rates), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT), ) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt index 18e7c56005..8c6125e33d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt @@ -7,10 +7,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags @Composable internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier) { @@ -19,7 +21,8 @@ internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier) .size(TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8) .background(TangemColorPalette.Light1) - .padding(TangemTheme.dimens.spacing6), + .padding(TangemTheme.dimens.spacing6) + .testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON), model = ImageRequest.Builder(context = LocalContext.current) .data(imageUrl) .crossfade(enable = true) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt index fb80254c3c..ee929788ee 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -11,11 +11,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig @@ -49,7 +51,7 @@ private fun SelectPaymentMethodBottomSheetContent( modifier: Modifier = Modifier, ) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST), ) { items( items = methods, @@ -86,7 +88,10 @@ private fun PaymentMethodItem(paymentMethod: PaymentMethodUM, isSelected: Boolea verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - PaymentMethodIcon(paymentMethod.imageUrl) + PaymentMethodIcon( + imageUrl = paymentMethod.imageUrl, + modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON), + ) Text( text = paymentMethod.name, style = TangemTheme.typography.subtitle2, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 59cb8cb0b0..116ca08de5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign @@ -32,6 +33,7 @@ import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon @@ -101,15 +103,19 @@ private fun PaymentMethodBlock( text = stringResourceSafe(id = R.string.onramp_pay_with), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_TITLE), ) Text( text = state.name, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME), ) } Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), + modifier = Modifier + .size(TangemTheme.dimens.size24) + .testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_EXPAND_BUTTON), painter = painterResource(id = R.drawable.ic_chevron_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -149,14 +155,16 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m modifier = modifier .selectedBorder(isSelected = state.isSelected) .clickable(onClick = state.onClick) - .padding(all = TangemTheme.dimens.spacing12), + .padding(all = TangemTheme.dimens.spacing12) + .testTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { SubcomposeAsyncImage( modifier = Modifier .size(size = TangemTheme.dimens.size40) - .clip(TangemTheme.shapes.roundedCorners8), + .clip(TangemTheme.shapes.roundedCorners8) + .testTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_NAME), model = ImageRequest.Builder(context = LocalContext.current) .data(state.imageUrl) .crossfade(enable = true) @@ -166,7 +174,9 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m contentDescription = null, ) Text( - modifier = Modifier.weight(1F), + modifier = Modifier + .weight(1F) + .testTag(SelectProviderBottomSheetTestTags.TOKEN_AMOUNT), text = state.name, style = TangemTheme.typography.subtitle2, color = if (state.isSelected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, @@ -189,7 +199,8 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(TangemTheme.colors.icon.accent) - .padding(vertical = 1.dp, horizontal = 6.dp), + .padding(vertical = 1.dp, horizontal = 6.dp) + .testTag(SelectProviderBottomSheetTestTags.BEST_RATE_LABEL), ) } state.diffRate != null -> { @@ -218,7 +229,8 @@ private fun UnavailableProviderItem( Row( modifier = modifier .selectedBorder(isSelected = isSelected) - .padding(all = TangemTheme.dimens.spacing12), + .padding(all = TangemTheme.dimens.spacing12) + .testTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_ITEM), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { @@ -240,11 +252,13 @@ private fun UnavailableProviderItem( text = providerName, style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_NAME), ) Text( text = subtitle.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_SUBTITLE), ) } } @@ -259,7 +273,8 @@ private fun OnrampMoreProviders() { contentDescription = null, tint = TangemTheme.colors.icon.informative, modifier = Modifier - .padding(top = 16.dp), + .padding(top = 16.dp) + .testTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_ICON), ) Text( text = stringResourceSafe(R.string.express_more_providers_soon), @@ -267,7 +282,8 @@ private fun OnrampMoreProviders() { color = TangemTheme.colors.icon.informative, modifier = Modifier .padding(top = 4.dp, bottom = 24.dp) - .padding(horizontal = TangemTheme.dimens.spacing56), + .padding(horizontal = TangemTheme.dimens.spacing56) + .testTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_TEXT), textAlign = TextAlign.Center, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index fec988b9d6..01c26e0e76 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -54,9 +54,8 @@ internal class OnrampRedirectModel @Inject constructor( resourceReference(R.string.common_buy), stringReference(" ${params.cryptoCurrency.name}"), ), - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = appRouter::pop, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = appRouter::pop, enabled = true, ), ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt index aaea4ccb7e..741e13aaa6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage @@ -25,6 +26,7 @@ import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcountry.entity.CountryItemState import com.tangem.features.onramp.selectcountry.entity.CountryListUM @@ -40,7 +42,7 @@ internal fun SelectCountryBottomSheet(config: TangemBottomSheetConfig, content: @Composable internal fun OnrampCountryList(state: CountryListUM, modifier: Modifier = Modifier) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(SelectCountryBottomSheetTestTags.LAZY_LIST), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), ) { item(key = "search_bar") { @@ -102,13 +104,15 @@ private fun LazyListScope.countryListWithContent(state: CountryListUM.Content) { modifier = Modifier .fillMaxWidth() .clickable(onClick = item.onClick) - .padding(vertical = TangemTheme.dimens.spacing16), + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(SelectCountryBottomSheetTestTags.COUNTRY_ITEM), state = item, ) is CountryItemState.WithContent.Unavailable -> UnavailableCountryItem( modifier = Modifier .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ITEM), state = item, ) } @@ -138,13 +142,17 @@ private fun ContentCountryItem(state: CountryItemState.WithContent.Content, modi verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( - modifier = Modifier.size(TangemTheme.dimens.size36), + modifier = Modifier + .size(TangemTheme.dimens.size36) + .testTag(SelectCountryBottomSheetTestTags.COUNTRY_ICON), model = state.flagUrl, contentDescription = null, ) Text( text = state.countryName, - modifier = Modifier.weight(1F), + modifier = Modifier + .weight(1F) + .testTag(SelectCountryBottomSheetTestTags.COUNTRY_NAME), overflow = TextOverflow.Ellipsis, maxLines = 1, style = TangemTheme.typography.subtitle2, @@ -172,7 +180,8 @@ private fun UnavailableCountryItem(state: CountryItemState.WithContent.Unavailab AsyncImage( modifier = Modifier .size(TangemTheme.dimens.size36) - .alpha(0.4F), + .alpha(0.4F) + .testTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ICON), model = state.flagUrl, contentDescription = null, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt index 8bcf48db89..cbc2cb8075 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer @@ -25,6 +26,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenFiatListTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState @@ -40,7 +42,7 @@ internal fun SelectCurrencyBottomSheet(config: TangemBottomSheetConfig, content: @Composable internal fun OnrampCurrencyList(state: CurrenciesListUM, modifier: Modifier = Modifier) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(BuyTokenFiatListTestTags.LAZY_LIST), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), ) { item(key = "search_bar") { @@ -106,7 +108,8 @@ private fun LazyListScope.currencyListContent(state: CurrenciesListUM.Content) { modifier = Modifier .fillMaxWidth() .clickable(onClick = item.onClick) - .padding(vertical = TangemTheme.dimens.spacing16), + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(BuyTokenFiatListTestTags.LAZY_LIST_ITEM), currency = item, ) }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 73b0f0a25a..890446c68e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -17,12 +17,12 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selecttoken.OnrampOperationComponent.Params @@ -57,7 +57,6 @@ internal class OnrampOperationModel @Inject constructor( private val selectedUserWallet = getWalletsUseCase.invokeSync() .first { it.walletId == params.userWalletId } - .requireColdWallet() init { analyticsEventHandler.send( @@ -154,7 +153,7 @@ internal class OnrampOperationModel @Inject constructor( } private fun showErrorIfDemoModeOrElse(action: () -> Unit) { - if (isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { + if (selectedUserWallet is UserWallet.Cold && isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { val alertUM = AlertDemoModeUM(onConfirmClick = {}) val message = DialogMessage( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt index d2c298c8f6..689cee2068 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -10,10 +10,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.features.onramp.hottokens.HotCryptoComponent import com.tangem.features.onramp.impl.R @@ -36,7 +38,8 @@ internal fun OnrampSelectToken( .nestedScroll(nestedScrollConnection) .background(TangemTheme.colors.background.secondary) .imePadding() - .systemBarsPadding(), + .systemBarsPadding() + .testTag(BuyTokenScreenTestTags.LAZY_LIST), ) { stickyHeader(key = "header") { AppBarWithBackButton( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt index 8a0d062131..33ba297a0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage @@ -19,6 +20,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.ResidenceSettingsScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.settings.entity.OnrampSettingsItemUM @@ -95,6 +97,7 @@ private fun ResidenceSection(state: OnrampSettingsItemUM.Residence, modifier: Mo text = state.countryName, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(ResidenceSettingsScreenTestTags.COUNTRY_NAME), ) Icon( painter = painterResource(id = R.drawable.ic_chevron_right_24), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt index 39ea071d73..36a1e410e8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt @@ -61,9 +61,8 @@ private fun Content(state: OnrampSuccessComponentUM.Content, onBackClick: () -> .systemBarsPadding(), topBar = { TangemTopAppBar( - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = onBackClick, ), ) }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt index 38a7a486de..35f44bbb33 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import kotlinx.coroutines.flow.StateFlow /** Token list component that present list of available tokens for swap */ diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt index 056bbaf84f..55e336791d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt @@ -3,7 +3,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt index b1a335134f..ccf527232b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.onramp.swap.availablepairs.entity.transformers -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt index 7e9c537c9e..d46f06637e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 12a9b31e16..18f0ca746d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -16,9 +16,9 @@ import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo @@ -200,7 +200,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .collectLatest { selectedStatus -> val networkInfo = selectedStatus.toLeastTokenInfo() - val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() ?: false + val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true if (isAlreadyLoaded) return@collectLatest val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt index 52a717c63e..5472e0e61c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.entity.SwapSelectTokensController import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt index e7daefe8c7..94ba06e247 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.onramp.tokenlist.entity.OnrampOperation diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 2deda8e910..70d687f450 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index b54f7d0386..f693f02e79 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -8,7 +8,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus /** [REDACTED_AUTHOR] diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 6a5103bcfa..ae9b3f2e32 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -13,15 +13,14 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetAssetRequirementsUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -63,7 +62,6 @@ internal class OnrampTokenListModel @Inject constructor( private val params: OnrampTokenListComponent.Params = paramsContainer.require() private val userWallet by lazy { getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - .requireColdWallet() // TODO [REDACTED_TASK_KEY] } init { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index c45da297d2..27943a21c4 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -24,6 +26,8 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider import kotlinx.collections.immutable.ImmutableList @@ -90,7 +94,9 @@ private fun ItemsBlock(items: ImmutableList, isBalanceHidden: lastIndex = items.lastIndex, addDefaultPadding = false, backgroundColor = TangemTheme.colors.background.primary, - ), + ) + .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + .semantics { lazyListItemPosition = index }, ) } } diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt index 9e347a47ba..2ab5d7832f 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt @@ -4,6 +4,7 @@ import com.tangem.common.routing.AppRoute data class PushNotificationsParams( val isBottomSheet: Boolean = false, + val nextRoute: AppRoute? = null, val modelCallbacks: PushNotificationsModelCallbacks, val source: AppRoute.PushNotification.Source, ) \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index dab4f52d73..c76c250f14 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -76,7 +76,7 @@ internal class PushNotificationsModel @Inject constructor( neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) params.modelCallbacks.onDenySystemPermission() if (!params.isBottomSheet) { - appRouter.push(AppRoute.Home) + params.nextRoute?.let { appRouter.push(it) } } } } @@ -90,7 +90,7 @@ internal class PushNotificationsModel @Inject constructor( neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) params.modelCallbacks.onAllowSystemPermission() if (!params.isBottomSheet) { - appRouter.push(AppRoute.Home) + params.nextRoute?.let { appRouter.push(it) } } } } @@ -104,7 +104,7 @@ internal class PushNotificationsModel @Inject constructor( neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) params.modelCallbacks.onDenySystemPermission() if (!params.isBottomSheet) { - appRouter.push(AppRoute.Home) + params.nextRoute?.let { appRouter.push(it) } } } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt index b26bb96aa2..7e11df9de8 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -66,9 +66,9 @@ internal fun QrScanningContent( TangemTopAppBar( modifier = Modifier.statusBarsPadding(), title = uiState.topBarConfig.title?.resolveReference(), - startButton = TopAppBarButtonUM( + startButton = TopAppBarButtonUM.Icon( iconRes = uiState.topBarConfig.startIcon, - onIconClicked = uiState.onBackClick, + onClicked = uiState.onBackClick, ), textColor = TangemTheme.colors.text.constantWhite, iconTint = TangemColorPalette.White, @@ -80,9 +80,9 @@ internal fun QrScanningContent( label = "Flash Change", ) { TopAppBarButton( - button = TopAppBarButtonUM( + button = TopAppBarButtonUM.Icon( iconRes = if (it) R.drawable.ic_flash_on_24 else R.drawable.ic_flash_off_24, - onIconClicked = { + onClicked = { isFlash = !isFlash }, ), @@ -91,9 +91,9 @@ internal fun QrScanningContent( } TopAppBarButton( - button = TopAppBarButtonUM( + button = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_gallery_24, - onIconClicked = uiState.onGalleryClick, + onClicked = uiState.onGalleryClick, ), tint = TangemColorPalette.White, ) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 083ea5d82b..9520d1847e 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.referral.domain import arrow.core.getOrElse import com.tangem.common.core.TangemSdkError -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt index 4e5166e070..bb4729aac8 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.referral.domain.di import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelComponent -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.ReferralInteractor diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 5bf12360cb..4aae138a4f 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -34,6 +35,7 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.ReferralProgramScreenTestTags import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException @@ -138,7 +140,8 @@ private fun Header() { modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing32) .fillMaxWidth() - .height(TangemTheme.dimens.size200), + .height(TangemTheme.dimens.size200) + .testTag(ReferralProgramScreenTestTags.IMAGE), ) SpacerH24() Text( @@ -233,7 +236,9 @@ private fun LoadingCondition(@DrawableRes iconResId: Int) { @Composable private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> Unit) { Row( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .testTag(ReferralProgramScreenTestTags.CONDITION_BLOCK), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), verticalAlignment = Alignment.Top, ) { @@ -268,6 +273,7 @@ private fun InfoForYou(award: String, networkName: String, address: String? = nu ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT), ) } } @@ -332,6 +338,7 @@ private fun ConditionInfo(title: String, subtitleContent: @Composable () -> Unit text = title, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, + modifier = Modifier.testTag(ReferralProgramScreenTestTags.INFO_FOR_YOUR_FRIEND_TEXT), ) subtitleContent() } diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index c1a6e00fc9..454fcee782 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { /** Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.analytics.models) /** Common */ implementation(projects.common.ui) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index fb665394be..294674ae19 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -3,5 +3,6 @@ package com.tangem.features.send.v2.api interface SendFeatureToggles { val isSendRedesignEnabled: Boolean + val isNFTSendRedesignEnabled: Boolean val isSendWithSwapEnabled: Boolean } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt index 1cef61c576..90823b9c00 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt @@ -6,9 +6,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.error.GetFeeError import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.StateFlow import java.math.BigDecimal diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt similarity index 87% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index acaf68a42f..7b355b9c83 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.common.analytics +package com.tangem.features.send.v2.api.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN @@ -6,9 +6,9 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM /** - * Send screen analytics + * Send analytics */ -internal sealed class CommonSendAnalyticEvents( +sealed class CommonSendAnalyticEvents( category: String, event: String, params: Map = mapOf(), @@ -114,12 +114,26 @@ internal sealed class CommonSendAnalyticEvents( ), ) + /** Token chosen to convert with sending */ + data class TokenChosen( + val categoryName: String, + val token: String, + val blockchain: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Token chosen", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + companion object { const val SEND_CATEGORY = "Token / Send" const val NFT_SEND_CATEGORY = "NFT" } - internal enum class SendScreenSource { + enum class SendScreenSource { Address, Amount, Fee, diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index 6386d23721..89ddb338b6 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -3,9 +3,13 @@ package com.tangem.features.send.v2.api.entity import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.R +import com.tangem.features.send.v2.api.entity.FeeItem.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal import java.math.BigInteger @@ -31,7 +35,19 @@ sealed class FeeSelectorUM { val feeExtraInfo: FeeExtraInfo, val feeFiatRateUM: FeeFiatRateUM?, val feeNonce: FeeNonce, - ) : FeeSelectorUM() + ) : FeeSelectorUM() { + fun toAnalyticType(): AnalyticsParam.FeeType = when (fees) { + is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed + is TransactionFee.Choosable -> when (selectedFeeItem) { + is Suggested, + is Custom, + -> AnalyticsParam.FeeType.Custom + is Fast -> AnalyticsParam.FeeType.Max + is Market -> AnalyticsParam.FeeType.Normal + is Slow -> AnalyticsParam.FeeType.Min + } + } + } } @Immutable @@ -58,14 +74,37 @@ sealed class FeeNonce { @Immutable sealed class FeeItem { abstract val fee: Fee + abstract val title: TextReference + abstract val iconRes: Int fun isSameClass(other: FeeItem): Boolean { return this::class == other::class } - data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem() - data class Slow(override val fee: Fee) : FeeItem() - data class Market(override val fee: Fee) : FeeItem() - data class Fast(override val fee: Fee) : FeeItem() - data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() + data class Suggested( + override val title: TextReference, + override val fee: Fee, + ) : FeeItem() { + override val iconRes: Int = R.drawable.ic_star_mini_24 + } + + data class Slow(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_slow) + override val iconRes: Int = R.drawable.ic_tortoise_24 + } + + data class Market(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_market) + override val iconRes: Int = R.drawable.ic_bird_24 + } + + data class Fast(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_fast) + override val iconRes: Int = R.drawable.ic_hare_24 + } + + data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_custom) + override val iconRes: Int = R.drawable.ic_edit_v2_24 + } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index 94c63eef08..80914b0ce9 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -4,7 +4,7 @@ import arrow.core.Either import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -16,6 +16,7 @@ sealed class FeeSelectorParams { abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus abstract val feeStateConfiguration: FeeStateConfiguration abstract val feeDisplaySource: FeeDisplaySource + abstract val analyticsCategoryName: String data class FeeSelectorBlockParams( override val state: FeeSelectorUM, @@ -24,6 +25,7 @@ sealed class FeeSelectorParams { override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, override val feeStateConfiguration: FeeStateConfiguration, override val feeDisplaySource: FeeDisplaySource, + override val analyticsCategoryName: String, ) : FeeSelectorParams() data class FeeSelectorDetailsParams( @@ -33,6 +35,7 @@ sealed class FeeSelectorParams { override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, override val feeStateConfiguration: FeeStateConfiguration, override val feeDisplaySource: FeeDisplaySource, + override val analyticsCategoryName: String, val callback: FeeSelectorModelCallback, ) : FeeSelectorParams() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt similarity index 70% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt index 97d65a88e9..55d91be4b5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt @@ -1,9 +1,9 @@ -package com.tangem.features.send.v2.subcomponents.amount.analytics +package com.tangem.features.send.v2.api.subcomponents.amount.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE -internal sealed class SendAmountAnalyticEvents( +sealed class CommonSendAmountAnalyticEvents( category: String, event: String, params: Map = mapOf(), @@ -13,7 +13,7 @@ internal sealed class SendAmountAnalyticEvents( data class SelectedCurrency( val categoryName: String, val type: SelectedCurrencyType, - ) : SendAmountAnalyticEvents( + ) : CommonSendAmountAnalyticEvents( category = categoryName, event = "Selected Currency", params = mapOf(TYPE to type.value), @@ -22,9 +22,9 @@ internal sealed class SendAmountAnalyticEvents( /** Max amount button clicked */ data class MaxAmountButtonClicked( val categoryName: String, - ) : SendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped") + ) : CommonSendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped") - internal enum class SelectedCurrencyType(val value: String) { + enum class SelectedCurrencyType(val value: String) { Token("Token"), AppCurrency("App Currency"), } diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt index 55cc94b302..b0bbb0fc76 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.api.subcomponents.destination.entity import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.utils.toBriefAddressFormat @Immutable sealed class DestinationTextFieldUM { @@ -27,6 +28,10 @@ sealed class DestinationTextFieldUM { val actualAddress: String get() = blockchainAddress ?: value + + // if value is human-readable address, this field contains the actual brief blockchain address + val briefBlockchainAddress: String? + get() = blockchainAddress?.toBriefAddressFormat(BRIEF_ADDRESS_EDGE_LENGTH, BRIEF_ADDRESS_EDGE_LENGTH) } data class RecipientMemo( @@ -40,4 +45,8 @@ sealed class DestinationTextFieldUM { val isEnabled: Boolean, val isValuePasted: Boolean, ) : DestinationTextFieldUM() + + private companion object { + const val BRIEF_ADDRESS_EDGE_LENGTH = 13 + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt similarity index 81% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt index c65789aaa4..579c005570 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt @@ -1,9 +1,9 @@ -package com.tangem.features.send.v2.subcomponents.fee.analytics +package com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -internal sealed class SendFeeAnalyticEvents( +sealed class CommonSendFeeAnalyticEvents( category: String, event: String, params: Map = mapOf(), @@ -15,7 +15,7 @@ internal sealed class SendFeeAnalyticEvents( data class SelectedFee( override val categoryName: String, val feeType: AnalyticsParam.FeeType, - ) : SendFeeAnalyticEvents( + ) : CommonSendFeeAnalyticEvents( category = categoryName, event = "Fee Selected", params = mapOf("Fee Type" to feeType.value), @@ -24,7 +24,7 @@ internal sealed class SendFeeAnalyticEvents( /** Custom fee selected */ data class CustomFeeButtonClicked( override val categoryName: String, - ) : SendFeeAnalyticEvents( + ) : CommonSendFeeAnalyticEvents( category = categoryName, event = "Custom Fee Clicked", ) @@ -32,7 +32,7 @@ internal sealed class SendFeeAnalyticEvents( /** Custom fee edited */ data class GasPriceInserter( override val categoryName: String, - ) : SendFeeAnalyticEvents( + ) : CommonSendFeeAnalyticEvents( category = categoryName, event = "Gas Price Inserted", ) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt index 181267b68a..3c251c609b 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.utils.extensions.isZero diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/utils/ConfirmFooterUtils.kt similarity index 89% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/utils/ConfirmFooterUtils.kt index 3c13220638..4b0d95ede3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/utils/ConfirmFooterUtils.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.common.utils +package com.tangem.features.send.v2.api.utils import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee @@ -8,10 +8,10 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.api.R import com.tangem.utils.StringsSigns.COMA_SIGN -internal fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference { +fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference { val suffix = when { fee.remainingEnergy == 0L -> { resourceReference( @@ -40,7 +40,7 @@ internal fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSend return combinedReference(prefix, stringReference("$COMA_SIGN "), suffix) } -internal fun formatFooterFiatFee( +fun formatFooterFiatFee( amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean, diff --git a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt index 59ea19d75c..76958ee240 100644 --- a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt +++ b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils import com.google.common.truth.Truth.assertThat -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import io.mockk.mockk import org.junit.jupiter.api.Test import java.math.BigDecimal diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index ef4d10fe2d..9d3830f696 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.features.txhistory.api) implementation(projects.features.nft.api) implementation(projects.features.swapV2.api) + implementation(projects.features.manageTokens.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index f414e52610..d73b45c69b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -8,6 +8,8 @@ internal class DefaultSendFeatureToggles( ) : SendFeatureToggles { override val isSendRedesignEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") + override val isNFTSendRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("NFT_SEND_REDESIGN_ENABLED") override val isSendWithSwapEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED") } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt new file mode 100644 index 0000000000..473a7b235e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R + +@Composable +internal fun FeeBlock(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + val feeExtraInfo = feeSelectorUM.feeExtraInfo + val feeFiatRateUM = feeSelectorUM.feeFiatRateUM + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount + SelectorRowItem( + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, + preDot = remember { + stringReference( + feeAmount.value.format { + crypto( + symbol = feeAmount.currencySymbol, + decimals = feeAmount.decimals, + ).fee(canBeLower = feeExtraInfo.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) { + getFiatReference(feeAmount.value, feeFiatRateUM.rate, feeFiatRateUM.appCurrency) + } else { + null + } + }, + ellipsizeOffset = feeAmount.currencySymbol.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 289b609dff..610020a85c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -5,17 +5,19 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.* +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.send.confirm.SendConfirmComponent -import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent @Composable internal fun SendContent( @@ -34,9 +36,9 @@ internal fun SendContent( Children( stack = stackState, animation = stackAnimation { child -> - when (child.instance) { - is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f) - is SendConfirmComponent -> fade() + when (child.configuration) { + is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f) + is CommonSendRoute.Confirm -> fade() else -> slide() } }, @@ -45,7 +47,14 @@ internal fun SendContent( it.instance.Content(Modifier.weight(1f)) } if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) { - SendNavigationButtons(navigationUM = navigationUM) + NavigationButtonsBlockV2( + navigationUM = navigationUM, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt deleted file mode 100644 index 31cf103e3d..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.ui.navigationButtons.NavigationButton -import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 -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.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.singleEvent - -@Composable -internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return - - Column( - modifier = modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - SendDoneButtons(navigationUM.secondaryPairButtonsUM) - SendNavigationButton( - navigationUM = navigationUM, - ) - } -} - -@Composable -private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - val navigationUM = navigationUM as? NavigationUM.Content ?: return - val primaryButton = navigationUM.primaryButton - - Row(modifier = modifier) { - AnimatedVisibility( - visible = navigationUM.prevButton != null, - enter = expandHorizontally(expandFrom = Alignment.End), - exit = shrinkHorizontally(shrinkTowards = Alignment.End), - ) { - val wrappedNavigationUM = remember(this) { requireNotNull(navigationUM.prevButton) } - Row { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_back_24)), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.button.secondary) - .clickableSingle(onClick = wrappedNavigationUM.onClick) - .padding(12.dp), - ) - SpacerW12() - } - } - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = { - if (primaryButton.isHapticClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - primaryButton.onClick() - }, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - ) - } -} - -@Composable -private fun SendDoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row(modifier = Modifier.padding(bottom = 12.dp)) { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = leftButton.iconRes!!, - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = rightButton.iconRes!!, - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt deleted file mode 100644 index da368be38b..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme - -@Composable -internal fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { - var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } - val keyboard by keyboardAsState() - - // the text should appear when the keyboard is closed - LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { - if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { - return@LaunchedEffect - } - isVisibleProxy = footerText != TextReference.EMPTY - } - - AnimatedVisibility( - visible = isVisibleProxy, - modifier = modifier, - enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), - exit = fadeOut(tween(durationMillis = 300)), - label = "Animate show sending state text", - ) { - Text( - text = footerText.resolveAnnotatedReference(), - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - ) - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index 4d66bc37aa..67396045c9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -1,18 +1,30 @@ package com.tangem.features.send.v2.entrypoint -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.plus +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent -import com.tangem.features.send.v2.entrypoint.model.SendEntryPoint +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel import com.tangem.features.swap.v2.api.SendWithSwapComponent import dagger.assisted.Assisted @@ -24,9 +36,17 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( @Assisted private val params: SendEntryPointComponent.Params, sendWithSwapComponentFactory: SendWithSwapComponent.Factory, sendComponentFactory: SendComponent.Factory, + private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, ) : SendEntryPointComponent, AppComponentContext by appComponentContext { - private val model: SendEntryPointModel = getOrCreateModel(params = params) + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val model: SendEntryPointModel = getOrCreateModel(params = params, router = innerRouter) private val sendWithSwapComponent = sendWithSwapComponentFactory.create( context = child("sendEntrySendWithSwap"), @@ -46,18 +66,77 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( ), ) + private val childStack = childStack( + key = "sendEntryStack", + source = stackNavigation, + serializer = null, + initialConfiguration = SendEntryRoute.Send, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + getChildComponent( + configuration = configuration, + factoryContext = childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + @Composable override fun Content(modifier: Modifier) { - val sendEntryState by model.sendEntryPointState.collectAsStateWithLifecycle() + val childStackValue by childStack.subscribeAsState() - sendComponent.Content(modifier) + Children( + stack = childStackValue, + animation = stackAnimation { child -> + when (child.configuration) { + SendEntryRoute.Send, + SendEntryRoute.SendWithSwap, + -> fade() + is SendEntryRoute.ChooseToken -> slide(orientation = Orientation.Vertical) + fade() + } + }, + ) { child -> + child.instance.Content(modifier.fillMaxSize()) + } + } - AnimatedVisibility( - visible = sendEntryState == SendEntryPoint.SendWithSwap, - enter = fadeIn(), - exit = fadeOut(), - ) { - sendWithSwapComponent.Content(modifier) + private fun getChildComponent( + configuration: SendEntryRoute, + factoryContext: AppComponentContext, + ): ComposableContentComponent = when (configuration) { + is SendEntryRoute.ChooseToken -> getManagedTokensComponent( + componentContext = factoryContext, + showSendViaSwapNotification = configuration.showSendViaSwapNotification, + ) + SendEntryRoute.Send -> sendComponent + SendEntryRoute.SendWithSwap -> sendWithSwapComponent + } + + private fun getManagedTokensComponent( + componentContext: ComponentContext, + showSendViaSwapNotification: Boolean, + ): ChooseManagedTokensComponent { + return chooseManagedTokensComponentFactory.create( + context = childByContext(componentContext), + params = ChooseManagedTokensComponent.Params( + userWalletId = params.userWalletId, + initialCurrency = params.cryptoCurrency, + source = ChooseManagedTokensComponent.Source.SendViaSwap, + selectedCurrency = null, + showSendViaSwapNotification = showSendViaSwapNotification, + callback = model, + analyticsCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY, + ), + ) + } + + private fun onChildBack() { + if (childStack.value.backStack.isEmpty()) { + router.pop() + } else { + stackNavigation.pop() } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt new file mode 100644 index 0000000000..470cfb6215 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2.entrypoint + +import com.tangem.core.decompose.navigation.Route + +internal sealed class SendEntryRoute : Route { + data object Send : SendEntryRoute() + data object SendWithSwap : SendEntryRoute() + data class ChooseToken( + val showSendViaSwapNotification: Boolean, + ) : SendEntryRoute() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 2f52fcdd3e..984330ba7d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -1,80 +1,74 @@ package com.tangem.features.send.v2.entrypoint.model -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent -import com.tangem.features.send.v2.api.SendEntryPointComponent +import com.tangem.features.send.v2.entrypoint.SendEntryRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger import com.tangem.features.swap.v2.api.SendWithSwapComponent -import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn import jakarta.inject.Inject -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@Suppress("LongParameterList") @ModelScoped internal class SendEntryPointModel @Inject constructor( - paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - val appRouter: AppRouter, - private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, + val router: Router, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, -) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback { + private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, +) : Model(), + SendComponent.ModelCallback, + SendWithSwapComponent.ModelCallback, + ChooseManagedTokensComponent.ModelCallback { - private val params: SendEntryPointComponent.Params = paramsContainer.require() - - val sendEntryPointState: StateFlow - field = MutableStateFlow(SendEntryPoint.SendVanilla) - - private var swapChooseTokenListenerJobHolder = JobHolder() + private var lastSavedAmount = "" override fun onConvertToAnotherToken(lastAmount: String) { - appRouter.push( - AppRoute.ChooseManagedTokens( - userWalletId = params.userWalletId, - initialCurrency = params.cryptoCurrency, - selectedCurrency = null, - source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, - ), - ) - observeChooseSelectToken(lastAmount) + lastSavedAmount = lastAmount + modelScope.launch { + val showSendViaSwapNotification = shouldShowNotificationUseCase( + NotificationId.SendViaSwapTokenSelectorNotification.key, + ) + router.push( + SendEntryRoute.ChooseToken( + showSendViaSwapNotification = showSendViaSwapNotification, + ), + ) + } } override fun onCloseSwap(lastAmount: String) { + lastSavedAmount = lastAmount modelScope.launch { if (lastAmount.isNotBlank()) { sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount) } - triggerScreenUpdate(SendEntryPoint.SendVanilla) + router.replaceAll(SendEntryRoute.Send) } } - private fun observeChooseSelectToken(lastAmount: String) { - swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow - .onEach { currency -> - if (lastAmount.isNotBlank()) { - swapAmountUpdateTrigger.triggerUpdateAmount(lastAmount) - } - triggerScreenUpdate(SendEntryPoint.SendWithSwap) + @Suppress("MagicNumber") + override fun onResult() { + modelScope.launch { + if (lastSavedAmount.isNotBlank()) { + swapAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount) } - .launchIn(modelScope) - .saveIn(swapChooseTokenListenerJobHolder) + // Workaround in order to execute correct exit animation on SendEntryRoute.ChooseToken + router.pop() + delay(10L) + router.replaceAll(SendEntryRoute.SendWithSwap) + } } - private fun triggerScreenUpdate(entry: SendEntryPoint) { - swapChooseTokenListenerJobHolder.cancel() - sendEntryPointState.update { entry } + override fun onBack() { + router.pop() } -} - -enum class SendEntryPoint { - SendVanilla, - SendWithSwap, } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 54be595d0a..5cb0f113e0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext @@ -19,6 +18,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent +import com.tangem.utils.extensions.isSingleItem import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -50,6 +50,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( callback = model, feeStateConfiguration = params.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, ), onDismiss = { model.feeSelectorBottomSheet.dismiss() @@ -73,13 +74,15 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( val state by model.uiState.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() + val isScreenSource = params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen + val isNotSingleFee = (state as? FeeSelectorUM.Content)?.feeItems?.isSingleItem() == false FeeSelectorBlockContent( state = state, onReadMoreClick = model::onReadMoreClicked, modifier = modifier - .conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) { + .conditional(isScreenSource && isNotSingleFee) { Modifier.clickable { - model.feeSelectorBottomSheet.activate(Unit) + model.showFeeSelector() } }, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index c655349774..2784044d44 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -3,8 +3,10 @@ package com.tangem.features.send.v2.feeselector.model import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.AmountType +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -12,13 +14,19 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadListener +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter import com.tangem.features.send.v2.feeselector.model.transformers.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update @@ -43,6 +51,7 @@ internal class FeeSelectorModel @Inject constructor( private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger, private val feeSelectorAlertFactory: FeeSelectorAlertFactory, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), FeeSelectorIntents, FeeSelectorModelCallback { private val params = paramsContainer.require() @@ -112,6 +121,11 @@ internal class FeeSelectorModel @Inject constructor( } override fun onFeeItemSelected(feeItem: FeeItem) { + if (feeItem is FeeItem.Custom) { + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = params.analyticsCategoryName), + ) + } uiState.update(FeeItemSelectedTransformer(feeItem)) } @@ -132,6 +146,27 @@ internal class FeeSelectorModel @Inject constructor( } override fun onDoneClick() { + val feeSelectorUM = uiState.value as? FeeSelectorUM.Content ?: return + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.SelectedFee( + categoryName = params.analyticsCategoryName, + feeType = feeSelectorUM.toAnalyticType(), + ), + ) + val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value + if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) { + analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName)) + } + if (feeSelectorUM.feeNonce is FeeNonce.Nonce) { + analyticsEventHandler.send( + NonceInserted( + categoryName = params.analyticsCategoryName, + token = params.feeCryptoCurrencyStatus.currency.symbol, + blockchain = params.feeCryptoCurrencyStatus.currency.network.name, + ), + ) + } + (params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value) } @@ -140,6 +175,19 @@ internal class FeeSelectorModel @Inject constructor( feeSelectorBottomSheet.dismiss() } + fun showFeeSelector() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.FeeScreenOpened(categoryName = params.analyticsCategoryName), + ) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Fee, + ), + ) + feeSelectorBottomSheet.activate(Unit) + } + private fun subscribeOnFeeReloadTriggerUpdates() { feeSelectorReloadListener.reloadTriggerFlow .onEach { data -> diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt index 864d53807d..139d171f9a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.feeselector.model.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt index 7a2265d453..258312e8a0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt index 54df9301fb..9418304d3b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.send.v2.feeselector.model.transformers import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt index e44f6cc4d0..69a2e8ece2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index b0f08978aa..2f403f5b0c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -39,6 +39,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.impl.R +import com.tangem.utils.extensions.isSingleItem import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -118,12 +119,11 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie text = annotatedString, modifier = Modifier .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), + .size(TangemTheme.dimens.size16) + .clip(CircleShape), content = { contentModifier -> Icon( - modifier = contentModifier - .size(TangemTheme.dimens.size16) - .clip(CircleShape), + modifier = contentModifier.size(TangemTheme.dimens.size16), painter = painterResource(id = R.drawable.ic_token_info_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -176,12 +176,14 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi textAlign = TextAlign.End, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) - Icon( - modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource(id = R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) + if (!state.feeItems.isSingleItem()) { + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } } } @@ -220,7 +222,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider { ), FeeSelectorUM.Content( isPrimaryButtonEnabled = false, - feeItems = persistentListOf(maxFeeItem), + feeItems = persistentListOf(lowFeeItem, maxFeeItem), selectedFeeItem = maxFeeItem, feeExtraInfo = FeeExtraInfo( isFeeApproximate = false, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 1c2ba7badd..c99b647b80 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -79,7 +79,7 @@ internal fun FeeSelectorModalBottomSheet( FeeSelectorItems( state = state, feeSelectorIntents = feeSelectorIntents, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 13.dp), + modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) }, footer = { @@ -115,7 +115,6 @@ private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDis } } -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun FeeSelectorItems( state: FeeSelectorUM.Content, @@ -141,114 +140,9 @@ private fun FeeSelectorItems( ) val itemModifier = Modifier .fillMaxWidth() - .background(TangemTheme.colors.background.primary) .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { - is FeeItem.Suggested -> RegularFeeItemContent( - modifier = itemModifier, - title = item.title, - iconRes = R.drawable.ic_star_mini_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Slow -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_slow), - iconRes = R.drawable.ic_tortoise_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Market -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_market), - iconRes = R.drawable.ic_bird_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Fast -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_fast), - iconRes = R.drawable.ic_hare_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) is FeeItem.Custom -> CustomFeeBlock( modifier = itemModifier, customFee = item, @@ -258,6 +152,32 @@ private fun FeeSelectorItems( onValueChange = feeSelectorIntents::onCustomFeeValueChange, nonce = state.feeNonce, ) + else -> RegularFeeItemContent( + modifier = itemModifier, + title = item.title, + iconRes = item.iconRes, + iconBackgroundColor = iconBackgroundColor, + iconTint = iconTint, + preDot = stringReference( + item.fee.amount.value.format { + crypto( + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + }, + ), + postDot = if (feeFiatRateUM != null) { + getFiatReference( + value = item.fee.amount.value, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, + ) + } else { + null + }, + ellipsizeOffset = item.fee.amount.currencySymbol.length, + showDivider = !isSelected && !lastItem, + ) } } } @@ -363,7 +283,7 @@ private fun ExpandedCustomFeeItems( showDivider = false, modifier = Modifier .background( - color = TangemTheme.colors.background.action, + color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium, ), ) @@ -495,7 +415,14 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), ), FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), - FeeItem.Market(fee = Fee.Common(Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum))), + FeeItem.Market( + fee = Fee.Common( + Amount( + value = BigDecimal("0.02"), + blockchain = Blockchain.Ethereum, + ), + ), + ), FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index c0d9b98bd2..74c7fa09ce 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -12,6 +12,7 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.ObserveLifecycleMode import com.arkivanov.decompose.value.subscribe import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child @@ -20,13 +21,13 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R @@ -37,7 +38,6 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted @@ -89,13 +89,15 @@ internal class DefaultSendComponent @AssistedInject constructor( ) { stack -> componentScope.launch { when (val activeComponent = stack.active.instance) { - is SendConfirmComponent -> if (model.currentRoute.value.isEditMode) { + is SendConfirmComponent -> { analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, ), ) - activeComponent.updateState(model.uiState.value) + if (model.currentRoute.value.isEditMode) { + activeComponent.updateState(model.uiState.value) + } } is SendAmountComponent -> { analyticsEventHandler.send( @@ -125,7 +127,11 @@ internal class DefaultSendComponent @AssistedInject constructor( val stackState by childStack.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = ::onChildBack) + BackHandler( + onBack = { + (state.navigationUM as? NavigationUM.Content)?.backIconClick() ?: onChildBack() + }, + ) SendContent( navigationUM = state.navigationUM, stackState = stackState, @@ -149,7 +155,7 @@ internal class DefaultSendComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, @@ -247,7 +253,6 @@ internal class DefaultSendComponent @AssistedInject constructor( val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value - val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value if (sendAmount == null || destinationAddress == null || @@ -272,29 +277,10 @@ internal class DefaultSendComponent @AssistedInject constructor( onClick = {}, ) - val feeBlockComponent = SendFeeBlockComponent( - appComponentContext = child("sendConfirmFeeBlock"), - params = SendFeeComponentParams.FeeBlockParams( - state = model.uiState.value.feeUM, - analyticsCategoryName = model.analyticCategoryName, - userWallet = model.userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - appCurrency = model.appCurrency, - sendAmount = sendAmount, - destinationAddress = destinationAddress, - blockClickEnableFlow = MutableStateFlow(true), - onLoadFee = model::loadFee, - ), - onResult = { }, - onClick = {}, - ) - return SendConfirmSuccessComponent( appComponentContext = factoryContext, params = SendConfirmSuccessComponent.Params( sendUMFlow = model.uiState, - feeBlockComponent = feeBlockComponent, destinationBlockComponent = destinationBlockComponent, analyticsCategoryName = model.analyticCategoryName, currentRoute = model.currentRoute, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 30a9234904..3a705b2270 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -7,7 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM import com.tangem.core.ui.extensions.capitalize -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents /** * Send screen analytics @@ -32,4 +32,15 @@ internal sealed class SendAnalyticEvents( NONCE to nonceNotEmpty.toString().capitalize(), ), ) + + data class ConvertTokenButtonClicked( + val token: String, + val blockchain: String, + ) : SendAnalyticEvents( + event = "Button - Convert Token", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index e98b5f4d20..a59c2345b7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -11,9 +11,9 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData @@ -104,6 +104,7 @@ internal class SendConfirmComponent( cryptoCurrencyStatus = params.cryptoCurrencyStatus, feeStateConfiguration = model.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, ), onResult = model::onFeeResult, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 8dac9245d8..1fb318f5c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase @@ -37,6 +38,8 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration @@ -47,8 +50,6 @@ import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificat import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper @@ -325,8 +326,12 @@ internal class SendConfirmModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) @@ -560,7 +565,7 @@ internal class SendConfirmModel @Inject constructor( state.copy( navigationUM = NavigationUM.Content( title = if (state.isRedesignEnabled) { - stringReference("") + resourceReference(id = R.string.common_send) } else { resourceReference( id = R.string.send_summary_title, @@ -589,7 +594,11 @@ internal class SendConfirmModel @Inject constructor( isValid = confirmUM.isPrimaryButtonEnabled, ), ) - appRouter.pop() + if (state.isRedesignEnabled) { + router.pop() + } else { + appRouter.pop() + } }, primaryButton = primaryButtonUM(), prevButton = null, @@ -623,7 +632,8 @@ internal class SendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, onClick = { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index 906f76bb91..98b863e56e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -14,10 +14,10 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.utils.formatFooterFiatFee -import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt index 7252942833..22e99488fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt @@ -12,12 +12,12 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.utils.formatFooterFiatFee -import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 21e776b4ad..7f7c70235b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -12,8 +13,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -22,7 +23,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.FeeSelectorBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -49,9 +49,13 @@ internal fun SendConfirmContent( ) { val confirmUM = sendUM.confirmUM as? ConfirmUM.Content - Column { + Column( + modifier = Modifier.fillMaxSize(), + ) { LazyColumn( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier + .weight(1f) + .padding(horizontal = TangemTheme.dimens.spacing16), ) { blocks( uiState = sendUM, @@ -74,7 +78,6 @@ internal fun SendConfirmContent( ) } } - SpacerHMax() SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 333bfe1c2a..f0a63844c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.requireColdWallet @@ -32,7 +33,6 @@ import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase @@ -41,6 +41,8 @@ import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent @@ -48,9 +50,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent import com.tangem.features.send.v2.send.ui.state.SendUM @@ -227,6 +228,12 @@ internal class SendModel @Inject constructor( } override fun onConvertToAnotherToken(lastAmount: String) { + analyticsEventHandler.send( + SendAnalyticEvents.ConvertTokenButtonClicked( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) params.callback?.onConvertToAnotherToken(lastAmount = lastAmount) } @@ -465,8 +472,12 @@ internal class SendModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt index c9fa321928..5468cbf45e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt @@ -12,7 +12,6 @@ import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel import com.tangem.features.send.v2.send.success.ui.SendConfirmSuccessContent import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +22,6 @@ internal class SendConfirmSuccessComponent( private val model: SendConfirmSuccessModel = getOrCreateModel(params = params) private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent - private val feeBlockComponent: SendFeeBlockComponent = params.feeBlockComponent @Composable override fun Content(modifier: Modifier) { @@ -31,14 +29,12 @@ internal class SendConfirmSuccessComponent( SendConfirmSuccessContent( sendUM = state, destinationBlockComponent = destinationBlockComponent, - feeBlockComponent = feeBlockComponent, ) } data class Params( val sendUMFlow: StateFlow, val destinationBlockComponent: SendDestinationBlockComponent, - val feeBlockComponent: SendFeeBlockComponent, val analyticsCategoryName: String, val currentRoute: Flow, val txUrl: String, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt index 663d111b4f..80bdafb04f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt @@ -12,14 +12,17 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject @Stable diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 862d4e1483..1d679a3e59 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -1,17 +1,18 @@ package com.tangem.features.send.v2.send.success.ui import androidx.compose.animation.* +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.ui.AmountBlock -import com.tangem.core.ui.components.SpacerHMax +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -20,19 +21,14 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.common.ui.SendNavigationButtons +import com.tangem.features.send.v2.common.ui.FeeBlock import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.delay @Composable -internal fun SendConfirmSuccessContent( - sendUM: SendUM, - destinationBlockComponent: SendDestinationBlockComponent, - feeBlockComponent: SendFeeBlockComponent, -) { +internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -50,13 +46,17 @@ internal fun SendConfirmSuccessContent( exit = slideOutVertically().plus(fadeOut()), label = "Animate success content", ) { - Column { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { Column( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .scrollable( state = rememberScrollState(), - orientation = Orientation.Horizontal, + orientation = Orientation.Vertical, ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -80,10 +80,20 @@ internal fun SendConfirmSuccessContent( onClick = {}, ) destinationBlockComponent.Content(modifier = Modifier) - feeBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = sendUM.feeSelectorUM) + Spacer(Modifier.height(60.dp)) } - SpacerHMax() - SendNavigationButtons(navigationUM = sendUM.navigationUM) + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index 60fe6511a2..b23440cfeb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -18,17 +18,17 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams @@ -42,7 +42,8 @@ import java.math.BigDecimal internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, - private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory, + private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory, private val analyticsEventHandler: AnalyticsEventHandler, ) : NFTSendComponent, AppComponentContext by appComponentContext { @@ -121,6 +122,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( is CommonSendRoute.Destination -> getDestinationComponent(factoryContext) is CommonSendRoute.Fee -> getFeeComponent(factoryContext) CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext) else -> getStubComponent() } @@ -164,9 +166,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( } } - private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent( + private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create( appComponentContext = factoryContext, - nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, params = NFTSendConfirmComponent.Params( state = model.uiState.value, analyticsCategoryName = analyticsCategoryName, @@ -180,9 +181,34 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( currentRoute = model.currentRouteFlow.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, onLoadFee = model::loadFee, + onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) }, ), ) + private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl + + if (txUrl == null) { + model.showAlertError() + return getStubComponent() + } + + return nftSendSuccessComponentFactory.create( + appComponentContext = factoryContext, + params = NFTSendSuccessComponent.Params( + nftSendUMFlow = model.uiState, + analyticsCategoryName = analyticsCategoryName, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + callback = model, + currentRoute = model.currentRouteFlow.filterIsInstance(), + txUrl = txUrl, + ), + ) + } + private fun getStubComponent() = ComposableContentComponent { } private fun onChildBack() { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt index de2ae7cbb4..f542b5f217 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt @@ -7,7 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM import com.tangem.core.ui.extensions.capitalize -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents /** * Send screen analytics diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index 7d5cc74f54..b57e306719 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -5,10 +5,10 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.Companion.NFT_SEND_CATEGORY import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.Companion.NFT_SEND_CATEGORY +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import javax.inject.Inject diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index fb7ed7c2b5..d2fd5670b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -10,31 +10,41 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.nft.models.NFTAsset -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent -import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent -import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* import java.math.BigDecimal -internal class NFTSendConfirmComponent( - appComponentContext: AppComponentContext, - params: Params, +internal class NFTSendConfirmComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: NFTSendConfirmModel = getOrCreateModel(params = params) @@ -74,12 +84,28 @@ internal class NFTSendConfirmComponent( onClick = model::showEditFee, ) + private val feeSelectorBlockComponent = feeSelectorComponentFactory.create( + context = child("NFTSendConfirmFeeSelectorBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = params.onLoadFee, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, + ), + onResult = model::onFeeResult, + ) + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( context = child("NFTDetailsBlock"), params = NFTDetailsBlockComponent.Params( userWalletId = params.userWallet.walletId, nftAsset = params.nftAsset, nftCollectionName = params.nftCollectionName, + isSuccessScreen = false, + title = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)), ), ) @@ -126,6 +152,7 @@ internal class NFTSendConfirmComponent( nftSendUM = state, destinationBlockComponent = destinationBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, notificationsComponent = notificationsComponent, notificationsUM = notificationState, @@ -145,9 +172,15 @@ internal class NFTSendConfirmComponent( val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val onLoadFee: suspend () -> Either, + val onSendTransaction: () -> Unit, ) interface ModelCallback { fun onResult(nftSendUM: NFTSendUM) } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendConfirmComponent + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index cdb172282b..d834d13885 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -21,23 +21,25 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.features.nft.entity.NFTSendSuccessTrigger import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.analytics.NFTSendAnalyticHelper @@ -60,6 +62,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @Suppress("LongParameterList", "LargeClass") @ModelScoped @@ -88,7 +91,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val sendFeeReloadTrigger: SendFeeReloadTrigger, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, -) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback { +) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback { private val params: NFTSendConfirmComponent.Params = paramsContainer.require() @@ -140,6 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor( updateConfirmNotifications() } + override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } updateConfirmNotifications() @@ -231,8 +240,12 @@ internal class NFTSendConfirmModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) @@ -395,13 +408,23 @@ internal class NFTSendConfirmModel @Inject constructor( ).onEach { (state, _) -> val confirmUM = state.confirmUM val confirmUMContent = confirmUM as? ConfirmUM.Content - val isReadyToSend = confirmUMContent != null && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( title = resourceReference(R.string.nft_send), - subtitle = confirmUMContent?.walletName, - backIconRes = R.drawable.ic_close_24, + subtitle = if (uiState.value.isRedesignEnabled) { + null + } else { + confirmUMContent?.walletName + }, + backIconRes = if (state.isRedesignEnabled) { + when (confirmUM) { + is ConfirmUM.Success -> R.drawable.ic_close_24 + else -> R.drawable.ic_back_24 + } + } else { + R.drawable.ic_close_24 + }, backIconClick = { analyticsEventHandler.send( CommonSendAnalyticEvents.CloseButtonClicked( @@ -411,25 +434,13 @@ internal class NFTSendConfirmModel @Inject constructor( isValid = confirmUM.isPrimaryButtonEnabled, ), ) - appRouter.pop() + if (state.isRedesignEnabled) { + router.pop() + } else { + appRouter.pop() + } }, - primaryButton = NavigationButton( - textReference = when (confirmUM) { - is ConfirmUM.Success -> resourceReference(R.string.common_close) - is ConfirmUM.Content -> if (confirmUM.isSending) { - resourceReference(R.string.send_sending) - } else { - resourceReference(R.string.common_send) - } - else -> resourceReference(R.string.common_send) - }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, - isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = isReadyToSend, - onClick = { - onNextClick(confirmUM) - }, - ), + primaryButton = primaryButtonUM(), prevButton = null, secondaryPairButtonsUM = ( NavigationButton( @@ -448,21 +459,40 @@ internal class NFTSendConfirmModel @Inject constructor( }.launchIn(modelScope) } - private fun onNextClick(confirmUM: ConfirmUM) { - when (confirmUM) { - is ConfirmUM.Success -> { - modelScope.launch { - nftSendSuccessTrigger.triggerSuccessNFTSend() + private fun primaryButtonUM(): NavigationButton { + val confirmUM = uiState.value.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending + return NavigationButton( + textReference = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) } - appRouter.pop() - } - is ConfirmUM.Content -> if (confirmUM.isSending) { - return - } else { - onSendClick() - } - else -> return - } + else -> resourceReference(R.string.common_send) + }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = isReadyToSend, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> { + modelScope.launch { + nftSendSuccessTrigger.triggerSuccessNFTSend() + } + appRouter.pop() + } + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ) } private companion object { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt index 561816abe5..9232bc21da 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -10,10 +10,10 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.utils.formatFooterFiatFee -import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 9dbb04eb6d..9ff84edfb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.sendnft.confirm.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -9,7 +10,9 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle @@ -20,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -40,6 +43,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { @@ -54,6 +58,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent = destinationBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { tapHelp(isDisplay = confirmUM.showTapHelp) @@ -79,31 +84,45 @@ private fun LazyListScope.blocks( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, ) { item(key = BLOCKS_KEY) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - AnimatedVisibility( - visible = nftSendUM.confirmUM is ConfirmUM.Success, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) { - val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } - TransactionDoneTitle( - title = resourceReference(R.string.sent_transaction_sent_title), - subtitle = resourceReference( - R.string.send_date_format, - wrappedList( - wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), - wrappedConfirmUM.transactionDate.toTimeFormat(), - ), - ), - modifier = Modifier.padding(vertical = 12.dp), + if (nftSendUM.isRedesignEnabled) { + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), ) + } else { + TransactionDoneTitleAnimated(nftSendUM = nftSendUM) + destinationBlockComponent.Content(modifier = Modifier) + nftDetailsBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) } - destinationBlockComponent.Content(modifier = Modifier) - - nftDetailsBlockComponent.Content(modifier = Modifier) - - feeBlockComponent.Content(modifier = Modifier) } } +} + +@Composable +private fun TransactionDoneTitleAnimated(nftSendUM: NFTSendUM) { + AnimatedVisibility( + visible = nftSendUM.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = 12.dp), + ) { + val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt index 3252c7bc71..5980fa7d22 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +24,9 @@ internal interface NFTSendModelModule { @IntoMap @ClassKey(NFTSendConfirmModel::class) fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model + + @Binds + @IntoMap + @ClassKey(NFTSendSuccessModel::class) + fun provideNFTSendSuccessModel(model: NFTSendSuccessModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 873a2c545e..90a8753b4a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -20,15 +20,17 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute @@ -36,6 +38,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -70,7 +73,8 @@ internal class NFTSendModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val alertFactory: SendConfirmAlertFactory, -) : Model(), SendNFTComponentCallback { + private val sendFeatureToggles: SendFeatureToggles, +) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -124,7 +128,7 @@ internal class NFTSendModel @Inject constructor( } else { when (currentRouteFlow.value) { is Destination -> router.push(Confirm) - Confirm -> router.push(ConfirmSuccess) + Confirm -> router.replaceAll(ConfirmSuccess) else -> onBackClick() } } @@ -193,6 +197,13 @@ internal class NFTSendModel @Inject constructor( } } + fun showAlertError() { + alertFactory.getGenericErrorState( + onFailedTxEmailClick = ::onFailedTxEmailClick, + popBack = router::pop, + ) + } + private fun onFailedTxEmailClick(errorMessage: String? = null) { saveBlockchainErrorUseCase( error = BlockchainErrorInfo( @@ -206,8 +217,12 @@ internal class NFTSendModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) @@ -245,7 +260,9 @@ internal class NFTSendModel @Inject constructor( private fun initialState(): NFTSendUM = NFTSendUM( destinationUM = DestinationUM.Empty(), feeUM = FeeUM.Empty(), + feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, + isRedesignEnabled = sendFeatureToggles.isNFTSendRedesignEnabled, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt new file mode 100644 index 0000000000..76f7329658 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt @@ -0,0 +1,96 @@ +package com.tangem.features.send.v2.sendnft.success + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel +import com.tangem.features.send.v2.sendnft.success.ui.NFTSendSuccessContent +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal class NFTSendSuccessComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, + nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: NFTSendSuccessModel = getOrCreateModel(params = params) + + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( + context = child("NFTDetailsSuccessBlock"), + params = NFTDetailsBlockComponent.Params( + userWalletId = params.userWallet.walletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + isSuccessScreen = true, + title = resourceReference(R.string.nft_asset), + ), + ) + + private val sendDestinationBlockComponent = sendDestinationBlockComponentFactory.create( + context = child("NFTDestinationSuccessBlock"), + params = DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = MutableStateFlow(false), + predefinedValues = PredefinedValues.Empty, + ), + onResult = {}, + onClick = {}, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + NFTSendSuccessContent( + nftSendUM = state, + destinationBlockComponent = sendDestinationBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + modifier = modifier, + ) + } + + data class Params( + val nftSendUMFlow: StateFlow, + val analyticsCategoryName: String, + val currentRoute: Flow, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val userWallet: UserWallet, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val txUrl: String, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onResult(nftSendUM: NFTSendUM) + } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendSuccessComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt new file mode 100644 index 0000000000..9352ec6898 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt @@ -0,0 +1,107 @@ +package com.tangem.features.send.v2.sendnft.success.model + +import androidx.compose.runtime.Stable +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@Stable +@ModelScoped +internal class NFTSendSuccessModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, +) : Model() { + private val params: NFTSendSuccessComponent.Params = paramsContainer.require() + + val uiState = params.nftSendUMFlow + + init { + configConfirmSuccessNavigation() + } + + private fun configConfirmSuccessNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).filter { it.second is CommonSendRoute.ConfirmSuccess }.onEach { (state, _) -> + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = stringReference(""), + subtitle = null, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = true, + ), + ) + appRouter.pop() + }, + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_close), + iconRes = null, + isEnabled = true, + isHapticClick = false, + onClick = { + appRouter.pop() + }, + ), + prevButton = null, + secondaryPairButtonsUM = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + onClick = ::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + onClick = ::onShareClick, + ), + ), + ), + ) + }.launchIn(modelScope) + } + + private fun onExploreClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName)) + urlOpener.openUrl(params.txUrl) + } + + private fun onShareClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName)) + shareManager.shareText(params.txUrl) + } + + interface ModelCallback { + fun onResult(sendUM: SendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt new file mode 100644 index 0000000000..10fa100bc9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -0,0 +1,103 @@ +package com.tangem.features.send.v2.sendnft.success.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toPx +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.common.ui.FeeBlock +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import kotlinx.coroutines.delay + +@Composable +internal fun NFTSendSuccessContent( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + modifier: Modifier = Modifier, +) { + var visible by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(ANIMATION_DELAY) + visible = true + } + + val height = ANIMATION_OFFSET.toPx().toInt() + + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { height }, + ).plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate success content", + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .scrollable( + state = rememberScrollState(), + orientation = Orientation.Vertical, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (nftSendUM.confirmUM is ConfirmUM.Success) { + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + nftSendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + nftSendUM.confirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM) + Spacer(Modifier.height(60.dp)) + } + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = nftSendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } + } +} + +private const val ANIMATION_DELAY = 600L +private val ANIMATION_OFFSET = (-40).dp \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index fb48eed197..45287e3c6e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,13 +1,16 @@ package com.tangem.features.send.v2.sendnft.ui.state import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal data class NFTSendUM( val destinationUM: DestinationUM, val feeUM: FeeUM, + val feeSelectorUM: FeeSelectorUM, val confirmUM: ConfirmUM, val navigationUM: NavigationUM, + val isRedesignEnabled: Boolean, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index d212be79aa..925d3780df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -3,11 +3,11 @@ package com.tangem.features.send.v2.subcomponents.amount import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback import kotlinx.coroutines.flow.StateFlow diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt index dadd0ea67c..f4373fe4b9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt @@ -2,9 +2,10 @@ package com.tangem.features.send.v2.subcomponents.amount.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.send.v2.impl.R import javax.inject.Inject @ModelScoped @@ -13,20 +14,19 @@ internal class SendAmountAlertFactory @Inject constructor( ) { fun showResetSendingAlert(onConfirm: () -> Unit) { - // todo fix localization [REDACTED_TASK_KEY] uiMessageSender.send( DialogMessage( - title = stringReference("Confirm Convert"), - message = stringReference("Proceed with conversion? Previous data will be reset."), + title = resourceReference(R.string.send_with_swap_convert_token_alert_title), + message = resourceReference(R.string.send_with_swap_convert_token_alert_message), firstActionBuilder = { EventMessageAction( - title = stringReference("Confirm"), + title = resourceReference(R.string.common_confirm), onClick = onConfirm, ) }, secondActionBuilder = { EventMessageAction( - title = stringReference("Not Now"), + title = resourceReference(R.string.common_not_now), onClick = onDismissRequest, ) }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 58c464e03a..72f75f5424 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -22,22 +22,22 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents.SelectedCurrencyType import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateListener -import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents -import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType import com.tangem.features.send.v2.subcomponents.fee.SendFeeData import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -218,7 +218,7 @@ internal class SendAmountModel @Inject constructor( ), ) analyticsEventHandler.send( - SendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName), + CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName), ) } @@ -229,7 +229,7 @@ internal class SendAmountModel @Inject constructor( override fun onAmountNext() { (uiState.value as? AmountState.Data)?.amountTextField?.isFiatValue?.let { isFiatSelected -> analyticsEventHandler.send( - SendAmountAnalyticEvents.SelectedCurrency( + CommonSendAmountAnalyticEvents.SelectedCurrency( categoryName = analyticsCategoryName, type = if (isFiatSelected) { SelectedCurrencyType.AppCurrency diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt index 489d2d0bc6..7dc514bb33 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt @@ -10,4 +10,7 @@ internal enum class EnterAddressSource { val isPasted: Boolean get() = this != InputField + + val isAutoNext: Boolean + get() = this == RecentAddress || this == MyWallets } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 6fdf7e3b09..d68153cd9e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -26,12 +26,12 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents @@ -145,6 +145,11 @@ internal class SendDestinationModel @Inject constructor( ) } + fun saveResult() { + val params = params as? SendDestinationComponentParams.DestinationParams ?: return + params.callback.onDestinationResult(uiState.value) + } + private fun initSenderAddress() { modelScope.launch { senderAddresses.value = getNetworkAddressesUseCase.invokeSync( @@ -281,18 +286,12 @@ internal class SendDestinationModel @Inject constructor( } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { - val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress && isValidMemo) { + if (type?.isAutoNext == true && isValidAddress && isValidMemo) { saveResult() (params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick() } } - private fun saveResult() { - val params = params as? SendDestinationComponentParams.DestinationParams ?: return - params.callback.onDestinationResult(uiState.value) - } - @Suppress("LongMethod") private fun configDestinationNavigation() { val params = params as? SendDestinationComponentParams.DestinationParams ?: return @@ -321,6 +320,7 @@ internal class SendDestinationModel @Inject constructor( isValid = state.isPrimaryButtonEnabled, ), ) + saveResult() } params.callback.onBackClick() }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt index e2cbe95c33..39cb1fd77b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -22,6 +22,10 @@ internal class SendDestinationInitialStateTransformer( Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field } + val placeholder = when (cryptoCurrency.network.nameResolvingType) { + Network.NameResolvingType.NONE -> resourceReference(R.string.send_enter_address_field) + Network.NameResolvingType.ENS -> resourceReference(R.string.send_enter_address_field_ens) + } return DestinationUM.Content( isPrimaryButtonEnabled = false, isInitialized = isInitialized, @@ -32,7 +36,7 @@ internal class SendDestinationInitialStateTransformer( keyboardType = KeyboardType.Text, ), error = null, - placeholder = resourceReference(R.string.send_enter_address_field), + placeholder = placeholder, label = resourceReference(R.string.send_recipient), isValuePasted = false, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index bb84ed1e1e..f8cf59bdb0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -62,7 +63,7 @@ private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) { Text( text = address.label.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Row( verticalAlignment = Alignment.CenterVertically, @@ -76,11 +77,21 @@ private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) { .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) .background(TangemTheme.colors.background.tertiary), ) - Text( - text = address.value, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + val blockchainAddress = address.briefBlockchainAddress + if (!blockchainAddress.isNullOrBlank()) { + Text( + text = blockchainAddress, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } } } @@ -94,7 +105,7 @@ private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) { Text( text = memo.label.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Text( text = memo.value, @@ -111,7 +122,7 @@ private fun AddressWithMemoBlock( memo: DestinationTextFieldUM.RecipientMemo?, ) { Text( - text = stringResourceSafe(R.string.send_to_address), + text = stringResourceSafe(R.string.send_recipient), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -120,12 +131,21 @@ private fun AddressWithMemoBlock( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) { - Text( - modifier = Modifier.weight(1f), - text = address.value, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = address.value, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + val blockchainAddress = address.briefBlockchainAddress + if (!blockchainAddress.isNullOrBlank()) { + Text( + text = blockchainAddress, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } IdentIcon( address = address.value, modifier = Modifier @@ -134,6 +154,7 @@ private fun AddressWithMemoBlock( .background(TangemTheme.colors.background.tertiary), ) } + if (memo != null && memo.value.isNotBlank()) { Text( text = stringResourceSafe(R.string.send_memo, memo.value), @@ -162,64 +183,51 @@ private fun DestinationBlockPreview( } private class DestinationBlockPreviewProvider : PreviewParameterProvider { + val previewItem = DestinationUM.Content( + isPrimaryButtonEnabled = true, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter address"), + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "Test memo for transaction", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter memo (optional)"), + label = TextReference.Str("Memo"), + isError = false, + error = null, + disabledText = TextReference.Str("Memo disabled"), + isEnabled = true, + isValuePasted = false, + ), + recent = emptyList().toImmutableList(), + wallets = emptyList().toImmutableList(), + networkName = "Ethereum", + isValidating = false, + isInitialized = true, + isRedesignEnabled = false, + ) + override val values: Sequence get() = sequenceOf( - DestinationUM.Content( - isPrimaryButtonEnabled = true, - addressTextField = DestinationTextFieldUM.RecipientAddress( - value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter address"), - label = TextReference.Str("Recipient Address"), - isError = false, - error = null, - isValuePasted = false, + previewItem, + previewItem.copy( + addressTextField = previewItem.addressTextField.copy( + value = "vitalik.eth", + blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", ), - memoTextField = DestinationTextFieldUM.RecipientMemo( - value = "Test memo for transaction", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter memo (optional)"), - label = TextReference.Str("Memo"), - isError = false, - error = null, - disabledText = TextReference.Str("Memo disabled"), - isEnabled = true, - isValuePasted = false, - ), - recent = emptyList().toImmutableList(), - wallets = emptyList().toImmutableList(), - networkName = "Ethereum", - isValidating = false, - isInitialized = true, - isRedesignEnabled = false, ), - DestinationUM.Content( - isPrimaryButtonEnabled = true, - addressTextField = DestinationTextFieldUM.RecipientAddress( - value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter address"), - label = TextReference.Str("Recipient Address"), - isError = false, - error = null, - isValuePasted = false, + previewItem.copy(isRedesignEnabled = true), + previewItem.copy( + addressTextField = previewItem.addressTextField.copy( + value = "vitalik.eth", + blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", ), - memoTextField = DestinationTextFieldUM.RecipientMemo( - value = "Test memo for transaction", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter memo (optional)"), - label = TextReference.Str("Memo"), - isError = false, - error = null, - disabledText = TextReference.Str("Memo disabled"), - isEnabled = true, - isValuePasted = false, - ), - recent = emptyList().toImmutableList(), - wallets = emptyList().toImmutableList(), - networkName = "Ethereum", - isValidating = false, - isInitialized = true, isRedesignEnabled = true, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt index 31966f7310..da247fa248 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -142,6 +142,7 @@ private fun LazyListScope.addressItem( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, ), + resolvedAddress = address.blockchainAddress, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt index bdf1038ebe..dcd8c7ca34 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt @@ -3,9 +3,9 @@ package com.tangem.features.send.v2.subcomponents.fee import arrow.core.Either import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import kotlinx.coroutines.flow.Flow diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt index 939bb73a64..11d3e108d9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.extensions.isZero diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index e1f722720f..5cad0c8511 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -11,14 +11,13 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.NonceInserted +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadListener -import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents -import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents.GasPriceInserter import com.tangem.features.send.v2.subcomponents.fee.model.transformers.* import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType @@ -112,7 +111,7 @@ internal class SendFeeModel @Inject constructor( updateFeeNotifications() if (feeType == FeeType.Custom) { analyticsEventHandler.send( - SendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = analyticsCategoryName), + CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = analyticsCategoryName), ) } } @@ -155,10 +154,12 @@ internal class SendFeeModel @Inject constructor( val isCustomFeeEdited = feeSelectorUM.selectedFee?.amount?.value != feeSelectorUM.fees.normal.amount.value if (feeSelectorUM.selectedType == FeeType.Custom && isCustomFeeEdited) { - analyticsEventHandler.send(GasPriceInserter(categoryName = analyticsCategoryName)) + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.GasPriceInserter(categoryName = analyticsCategoryName), + ) } analyticsEventHandler.send( - SendFeeAnalyticEvents.SelectedFee( + CommonSendFeeAnalyticEvents.SelectedFee( categoryName = analyticsCategoryName, feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM), ), @@ -166,7 +167,7 @@ internal class SendFeeModel @Inject constructor( if (feeSelectorUM.nonce != null) { analyticsEventHandler.send( - NonceInserted( + CommonSendAnalyticEvents.NonceInserted( categoryName = analyticsCategoryName, token = cryptoCurrencyStatus.currency.symbol, blockchain = cryptoCurrencyStatus.currency.network.name, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt index d66b9376e2..7891a1fd2c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt @@ -3,9 +3,9 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt index 820e44dbb4..344269f818 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -4,12 +4,12 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index efb7fee254..821aac24fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index b16dd562a8..85cea0393c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -9,10 +9,10 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt index 929863fee3..3147b5eabc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt index 759cf62c33..de65e73033 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -10,7 +10,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT @@ -18,7 +19,6 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index c3dd23ba75..66607e3750 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -10,10 +10,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt index 65d07205da..495efa22af 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents -import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter import com.tangem.utils.transformer.Transformer internal class SendFeeCustomAutoFixTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt index 5d95767e59..1569cd7d3b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt @@ -1,11 +1,11 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.utils.transformer.Transformer internal class SendFeeCustomValueChangeTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt index 2a2b88a7d2..498ced2b6b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.lib.crypto.BlockchainUtils.isTron diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt index 89a69d49f0..3c16c06146 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt index 6ae4b1b0d3..853aff08fe 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.utils.transformer.Transformer internal class SendFeeSelectTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt index 10284bde92..5aae6a80e5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -14,6 +15,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN @@ -62,20 +64,24 @@ internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 } SelectorRowItem( - titleRes = title, + title = resourceReference(title), iconRes = icon, - preDot = stringReference( - feeAmount?.value.format { - crypto( - symbol = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - ).fee(canBeLower = feeUM.isFeeApproximate) - }, - ), - postDot = if (feeUM.isFeeConvertibleToFiat) { - getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) - } else { - null + preDot = remember { + stringReference( + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeUM.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeUM.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) + } else { + null + } }, ellipsizeOffset = feeAmount?.currencySymbol?.length, isSelected = true, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt index 3cc6d6042b..09ae784965 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt @@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN import com.tangem.core.ui.format.bigdecimal.crypto @@ -52,7 +53,7 @@ internal fun SendSpeedSelectorItem( .clickable { onSelect() }, ) { SelectorRowItem( - titleRes = titleRes, + title = resourceReference(titleRes), iconRes = iconRes, onSelect = onSelect, modifier = modifier, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 8f2dabbed3..da00a9307b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -25,12 +25,12 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt index 6eece69628..020d3afcab 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt @@ -205,6 +205,7 @@ class SendConfirmationNotificationsTransformerTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 873cfb305a..6903fc2550 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -204,6 +204,7 @@ class SendConfirmationNotificationsTransformerV2Test { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt index 79cba32b78..b07143a6b1 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt @@ -307,6 +307,7 @@ class TransformersComparisonTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 762201aacb..995576c43d 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(deps.androidx.paging.runtime) /** Other dependencies */ + implementation(deps.kotlin.datetime) implementation(deps.kotlin.immutable.collections) implementation(deps.material) implementation(deps.arrow.core) @@ -27,7 +28,6 @@ dependencies { /** Compose */ implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.material3) - implementation(deps.compose.material) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 3c56d06c66..131d64fb11 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -5,11 +5,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* internal class StakingAnalyticSender( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 8b99cc5560..31e0e9e58c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -29,25 +29,28 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.* +import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.staking.* import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor @@ -103,7 +106,6 @@ internal class StakingModel @Inject constructor( private val sendTransactionUseCase: SendTransactionUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, - private val isApproveNeededUseCase: IsApproveNeededUseCase, private val vibratorHapticManager: VibratorHapticManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, @@ -534,7 +536,7 @@ internal class StakingModel @Inject constructor( getActionRequirementAmountUseCase.invoke( integrationId = yieldBalance.integrationId, actionType = StakingActionType.CLAIM_REWARDS, - ).getOrNull() + ) } else { minimumAmount } @@ -857,6 +859,10 @@ internal class StakingModel @Inject constructor( modelScope.launch { val network = cryptoCurrencyStatus.currency.network + if (userWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) .getOrElse { error("CardInfo must be not null") } @@ -904,21 +910,18 @@ internal class StakingModel @Inject constructor( } private suspend fun setupApprovalNeeded() { - stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).fold( - ifRight = { approval -> - if (approval is StakingApproval.Needed) { - stakingAllowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - } - approval - }, - ifLeft = { - StakingApproval.Empty - }, - ) + val approval = StakingIntegrationID.create(currencyId = cryptoCurrencyStatus.currency.id)?.approval + ?: StakingApproval.Empty + + stakingApproval = approval + + if (approval is StakingApproval.Needed) { + stakingAllowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + } } private suspend fun setupIsAnyTokenStaked() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 7b669ddab9..4be332b317 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -2,6 +2,10 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.PendingActionConstraints +import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.staking.model.stakekit.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index e4bc6089b6..3f8262760b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt index 873ba40422..6601a859f6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.bottomsheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction internal data class StakingActionSelectionBottomSheetConfig( val title: TextReference, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 8c08dc5acf..5d548f7ab4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -6,14 +6,14 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.BalanceType.Companion.isClickable +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.utils.getRewardStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.lib.crypto.BlockchainUtils @@ -22,7 +22,7 @@ import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toPersistentList -import org.joda.time.DateTime +import kotlinx.datetime.Instant import java.math.BigDecimal import java.util.Calendar @@ -123,7 +123,7 @@ internal class BalanceItemConverter( -> null } - private fun getUnbondingDate(date: DateTime?): TextReference? { + private fun getUnbondingDate(date: Instant?): TextReference? { val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null if (date == null) { return combinedReference( @@ -137,7 +137,7 @@ internal class BalanceItemConverter( nowCalendar.resetHours() val endDate = Calendar.getInstance() - endDate.timeInMillis = date.millis + endDate.timeInMillis = date.toEpochMilliseconds() endDate.resetHours() val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 1a23a879e8..4c47f26565 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -2,15 +2,15 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -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.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider @@ -69,11 +69,12 @@ internal class RewardsValidatorStateConverter( }, ) val formattedFiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ) return BalanceState( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index ee906adb33..4251a689ff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -5,10 +5,14 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.* -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.RewardBlockType +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.YieldReward import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable @@ -50,7 +54,7 @@ internal class YieldBalancesConverter( ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } InnerYieldBalanceState.Data( - integrationId = yieldBalance?.integrationId, + integrationId = yieldBalance?.stakingId?.integrationId, reward = YieldReward( rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, rewardsFiat = fiatRewardsValue.format { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index 0cd6d2468c..3defda6f74 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -1,14 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.helpers +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.FetchActionsUseCase import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchPendingTransactionsUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.utils.coroutines.DelayedWork import dagger.assisted.Assisted diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index 1b9528d027..d4f2c4da10 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -8,20 +8,20 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.EstimateGasUseCase -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index 74cfa8e53b..8223f07a1d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -5,13 +5,15 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase import com.tangem.domain.staking.GetStakingTransactionsUseCase import com.tangem.domain.staking.SaveUnsubmittedHashUseCase import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.SubmitHashData -import com.tangem.domain.staking.model.stakekit.NetworkType -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -19,14 +21,12 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 7ad9912c1a..4deacdff09 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.RewardBlockType +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index d4ca71cd71..936fea5c1b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -7,12 +7,12 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.model.StakingClickIntents +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 9c1ea68323..4c0166c007 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -13,8 +13,6 @@ import com.tangem.features.staking.impl.presentation.state.utils.getPendingActio import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal class SetButtonsStateTransformer( private val urlOpener: UrlOpener, @@ -23,12 +21,13 @@ internal class SetButtonsStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl val buttonsState = if (prevState.isButtonsVisible()) { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), prevButton = getPrevButton(prevState), - extraButtons = getExtraButtons(prevState), - txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, + extraButtons = getExtraButtons(prevState).takeIf { txUrl != null }, + txUrl = txUrl, onTextClick = urlOpener::openUrl, ) } else { @@ -77,26 +76,23 @@ internal class SetButtonsStateTransformer( ).takeIf { prevState.currentStep.isPrevButtonVisible() } } - private fun getExtraButtons(prevState: StakingUiState): ImmutableList { - return persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_web_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onExploreClick, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_share_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onShareClick, - ), + private fun getExtraButtons(prevState: StakingUiState): Pair { + return NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onShareClick, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index c9a8054cc5..73b8c296f2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 7190ad5ae1..7eb0589c34 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index ba40e2960c..7fb48b2a61 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.features.staking.impl.presentation.state.utils.isTronStakedBalance diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index 327ec4ae8e..db0da4876d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index b13bae78df..47475d3b74 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -14,10 +14,10 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt index c70861712b..07077a8575 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index e905b0b0a5..0603ec9e7f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt index 0410bf8745..9631543416 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 318d0c6522..cee87196ee 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt index f5e92f66ec..ce81a3c55c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt index 5f7b5e5e57..026ab044df 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index 321db52473..f24052ac95 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -11,10 +11,10 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.extensions.isPositive diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt index d2b2e7fe50..26deb1759b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import java.math.RoundingMode diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 0729792529..235472ef08 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.approva import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index be9aac4f36..c39ac9a72f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -6,10 +6,10 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -38,11 +38,12 @@ internal class ShowApprovalBottomSheetTransformer( val feeCryptoValue = fee.amount.value.format { crypto(fee.amount.currencySymbol, fee.amount.decimals) } - val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), - fiatCurrencyCode = appCurrencyProvider().code, - fiatCurrencySymbol = appCurrencyProvider().symbol, - ) + val feeFiatValue = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value).format { + fiat( + fiatCurrencyCode = appCurrencyProvider().code, + fiatCurrencySymbol = appCurrencyProvider().symbol, + ) + } return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( isShown = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 122420439f..0dbf76f8ad 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -14,11 +14,11 @@ import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.StakingErrors import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 10e064d094..8959e90124 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -4,12 +4,12 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingNotification diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index 9ef8d57a41..c940c12e8a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.validat import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt index b4d42058e1..3f99aba73d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.utils -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.lib.crypto.BlockchainUtils.isTron import java.math.BigDecimal import java.math.MathContext diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index da281e339d..6ef20b93a9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.utils import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isCardano diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 1cccaf852d..27ba05eb91 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -42,8 +43,9 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.RewardBlockType +import com.tangem.core.ui.test.StakingDetailsScreenTestTags +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.RewardBlockType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.BalanceState @@ -171,7 +173,8 @@ private fun LazyListScope.activeStakingBlock( currentIndex = index + 1, lastIndex = state.yieldBalance.balances.lastIndex + 1, addDefaultPadding = false, - ), + ) + .testTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK), ) } } @@ -189,7 +192,9 @@ private fun BannerBlock(onClick: () -> Unit) { ), ) { Image( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(StakingDetailsScreenTestTags.BANNER_IMAGE), contentScale = ContentScale.FillWidth, painter = painterResource(R.drawable.img_staking_banner), contentDescription = null, @@ -197,7 +202,8 @@ private fun BannerBlock(onClick: () -> Unit) { Text( modifier = Modifier .align(Alignment.CenterStart) - .padding(TangemTheme.dimens.spacing16), + .padding(TangemTheme.dimens.spacing16) + .testTag(StakingDetailsScreenTestTags.BANNER_TEXT), text = buildAnnotatedString { withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) { append(stringResourceSafe(R.string.staking_details_banner_text)) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 9cc528d859..e1a7c81bd2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig @@ -20,6 +21,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep @@ -44,7 +46,8 @@ internal fun StakingScreen(uiState: StakingUiState) { .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() .imePadding() - .systemBarsPadding(), + .systemBarsPadding() + .testTag(StakingSendScreenTestTags.SCREEN_CONTAINER), horizontalAlignment = Alignment.CenterHorizontally, ) { StakingAppBar( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt index c7e67f5415..b09a9ab029 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt @@ -2,11 +2,14 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.features.staking.impl.R private const val TERMS_OF_USE_KEY = "termsOfUse" @@ -58,5 +61,6 @@ internal fun StakingTosText(onTextClick: (String) -> Unit) { onTextClick(PRIVACY_POLICY_URL) } }, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.TOS_TEXT), ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index bce18ef1ec..7efc5b644c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -9,6 +9,7 @@ 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.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -19,6 +20,7 @@ import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto @@ -26,9 +28,10 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.utils.StringsSigns.DASH_SIGN import java.math.BigDecimal @Composable @@ -38,7 +41,8 @@ internal fun StakingFeeBlock(feeState: FeeState) { .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK), ) { Text( text = stringResourceSafe(R.string.common_network_fee_title), @@ -51,7 +55,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = stringReference( feeAmount?.value.format { @@ -75,7 +79,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Loading -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), @@ -85,7 +89,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Error -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), @@ -126,7 +130,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { ) { if (it == FeeState.Error) { Text( - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 6e2d13e5e0..7182fc7bf4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder @@ -35,7 +37,8 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, - ), + ) + .testTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK), ) { InputRowImageInfo( title = resourceReference(R.string.staking_validator), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt index 9a1c63bc5b..ae9fc0d984 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt @@ -17,8 +17,8 @@ import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt index 15418e847b..8fa0dc468d 100644 --- a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt @@ -12,8 +12,10 @@ interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, val initialCurrency: CryptoCurrency, + val analyticsCategoryName: String, val selectedCurrency: CryptoCurrency?, val token: ManagedCryptoCurrency.Token, + val isSearchedToken: Boolean, val onDismiss: () -> Unit, val onResult: (SwapCurrencies, CryptoCurrency) -> Unit, ) diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index c1d107eaf9..fed1e814da 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -29,6 +29,8 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.navigation) implementation(projects.core.configToggles) + implementation(projects.core.datasource) + implementation(projects.core.analytics) /** Common */ implementation(projects.common.ui) @@ -63,6 +65,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.txhistory.models) implementation(projects.domain.txhistory) + implementation(projects.domain.notifications) implementation(projects.domain.feedback.models) implementation(projects.domain.feedback) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt index dbbf511732..4df2de3238 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe @@ -17,6 +16,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams.AmountBlockParams import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel @@ -67,19 +67,8 @@ internal class SwapAmountBlockComponent( onInfoClick = model::onInfoClick, isClickEnabled = isClickEnabled, onClick = onClick, - onProviderSelectClick = { - val amountUM = model.uiState.value as? SwapAmountUM.Content ?: return@SwapAmountBlockContent - val selectedProvider = amountUM.selectedQuote.provider ?: return@SwapAmountBlockContent - val cryptoCurrency = params.secondaryCryptoCurrency ?: return@SwapAmountBlockContent - - model.bottomSheetNavigation.activate( - SwapChooseProviderConfig( - providers = amountUM.swapQuotes, - cryptoCurrency = cryptoCurrency, - selectedProvider = selectedProvider, - ), - ) - }, + onFinishAnimation = model::onFinishAnimation, + onProviderSelectClick = model::onProviderClick, ) bottomSheet.child?.instance?.BottomSheet() @@ -95,6 +84,7 @@ internal class SwapAmountBlockComponent( providers = config.providers, cryptoCurrency = config.cryptoCurrency, selectedProvider = config.selectedProvider, + userCountry = config.userCountry, callback = model, onDismiss = { model.bottomSheetNavigation.dismiss() }, ), @@ -105,5 +95,6 @@ internal class SwapAmountBlockComponent( val providers: ImmutableList, val cryptoCurrency: CryptoCurrency, val selectedProvider: ExpressProvider, + val userCountry: UserCountry, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index c5e112b2f4..109c84dd81 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -3,9 +3,9 @@ package com.tangem.features.swap.v2.impl.amount import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt new file mode 100644 index 0000000000..197bd8a27c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt @@ -0,0 +1,27 @@ +package com.tangem.features.swap.v2.impl.amount.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER + +internal sealed class SwapAmountAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + data class ProviderSelectorClicked( + val categoryName: String, + ) : SwapAmountAnalyticEvents( + category = categoryName, + event = "Provider Clicked", + ) + + data class ProviderChosen( + val categoryName: String, + val providerName: String, + ) : SwapAmountAnalyticEvents( + category = categoryName, + event = "Provider Chosen", + params = mapOf(PROVIDER to providerName), + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 26267a4710..147e318a85 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -6,9 +6,9 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.ImmutableList @@ -48,9 +48,11 @@ internal sealed class SwapAmountUM { val swapCurrencies: SwapCurrencies, val swapQuotes: ImmutableList, val selectedQuote: SwapQuoteUM, + val showFCAWarning: Boolean, // extra data val appCurrency: AppCurrency?, + val showBestRateAnimation: Boolean, ) : SwapAmountUM() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountAlertFactory.kt index 8b467ba82b..258f59acc7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountAlertFactory.kt @@ -64,22 +64,19 @@ internal class SwapAmountAlertFactory @Inject constructor( } fun showCloseSendWithSwapAlert(onConfirm: () -> Unit) { - // todo fix localization [REDACTED_TASK_KEY] uiMessageSender.send( DialogMessage( - title = stringReference("Confirm cancellation"), - message = stringReference( - "Are you sure you want to cancel the conversion? After changing, previous data will be reset.", - ), + title = resourceReference(R.string.send_with_swap_remove_convert_alert_title), + message = resourceReference(R.string.send_with_swap_remove_convert_alert_message), firstActionBuilder = { EventMessageAction( - title = stringReference("Confirm"), + title = resourceReference(R.string.common_confirm), onClick = onConfirm, ) }, secondActionBuilder = { EventMessageAction( - title = stringReference("Not Now"), + title = resourceReference(R.string.common_not_now), onClick = onDismissRequest, ) }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index 7e5a767bb6..cf6b75586e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -9,4 +9,5 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onInfoClick() fun onSelectTokenClick() fun onSeparatorClick() + fun onProviderClick() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index dfc08a58f3..3c4c055991 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.swap.v2.impl.amount.model import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter @@ -9,14 +10,22 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference +import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection @@ -25,8 +34,8 @@ import com.tangem.domain.swap.models.getGroupWithDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.impl.R @@ -34,6 +43,7 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChoo import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceListener import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener +import com.tangem.features.swap.v2.impl.amount.analytics.SwapAmountAnalyticEvents import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM @@ -47,6 +57,7 @@ import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.extensions.orZero +import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.update import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -54,6 +65,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal +import java.util.Locale import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -69,12 +81,16 @@ internal class SwapAmountModel @Inject constructor( private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, private val getAllowanceUseCase: GetAllowanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getUserCountryUseCase: GetUserCountryUseCase, + private val swapBestRateAnimationStore: SwapBestRateAnimationStore, private val appRouter: AppRouter, private val swapAmountAlertFactory: SwapAmountAlertFactory, private val swapAlertFactory: SwapAlertFactory, private val swapAmountUpdateListener: SwapAmountUpdateListener, private val swapAmountReduceListener: SwapAmountReduceListener, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, + private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -89,8 +105,11 @@ internal class SwapAmountModel @Inject constructor( private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null + var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var showBestRateAnimation: Boolean = false + val uiState: StateFlow field = MutableStateFlow(params.amountUM) @@ -100,6 +119,9 @@ internal class SwapAmountModel @Inject constructor( init { modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + userCountry = getUserCountryUseCase.invokeSync().getOrNull() + ?: UserCountry.Other(Locale.getDefault().country) + showBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull() } configAmountNavigation() subscribeOnCryptoCurrencyStatusFlow() @@ -129,11 +151,18 @@ internal class SwapAmountModel @Inject constructor( } override fun onProviderResult(quoteUM: SwapQuoteUM) { + analyticsEventHandler.send( + SwapAmountAnalyticEvents.ProviderChosen( + categoryName = params.analyticsCategoryName, + providerName = quoteUM.provider?.name.orEmpty(), + ), + ) uiState.transformerUpdate( SwapAmountSelectQuoteTransformer( quoteUM = quoteUM, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), ), ) } @@ -168,10 +197,10 @@ internal class SwapAmountModel @Inject constructor( value = value, ), ) + quoteTaskScheduler.cancelTask() amountDebouncer.debounce( coroutineScope = modelScope, waitMs = DEBOUNCE_AMOUNT_DELAY, - forceUpdate = true, destinationFunction = { startLoadingQuotesTask(isSilentReload = false) }, @@ -183,6 +212,9 @@ internal class SwapAmountModel @Inject constructor( } override fun onMaxValueClick() { + analyticsEventHandler.send( + CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = params.analyticsCategoryName), + ) uiState.transformerUpdate( SwapAmountValueMaxTransformer( primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, @@ -203,12 +235,32 @@ internal class SwapAmountModel @Inject constructor( } override fun onAmountNext() { + val amountState = uiState.value.swapDirection.withSwapDirection( + onDirect = { uiState.value.primaryAmount.amountField }, + onReverse = { uiState.value.secondaryAmount.amountField }, + ) as? AmountState.Data + + amountState?.amountTextField?.isFiatValue?.let { isFiatSelected -> + analyticsEventHandler.send( + CommonSendAmountAnalyticEvents.SelectedCurrency( + categoryName = params.analyticsCategoryName, + type = if (isFiatSelected) { + CommonSendAmountAnalyticEvents.SelectedCurrencyType.AppCurrency + } else { + CommonSendAmountAnalyticEvents.SelectedCurrencyType.Token + }, + ), + ) + } saveResult() } override fun onSelectTokenClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return modelScope.launch { + val showSendViaSwapNotification = shouldShowNotificationUseCase( + NotificationId.SendViaSwapTokenSelectorNotification.key, + ) val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true val selectedCurrency = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus?.currency appRouter.push( @@ -217,6 +269,8 @@ internal class SwapAmountModel @Inject constructor( initialCurrency = primaryCryptoCurrency, selectedCurrency = selectedCurrency.takeIf { isEditMode }, source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + showSendViaSwapNotification = showSendViaSwapNotification, + analyticsCategoryName = params.analyticsCategoryName, ), ) } @@ -238,6 +292,33 @@ internal class SwapAmountModel @Inject constructor( } } + fun onFinishAnimation() { + uiState.update { + (it as? SwapAmountUM.Content)?.copy(showBestRateAnimation = false) ?: it + } + } + + override fun onProviderClick() { + val amountUM = uiState.value as? SwapAmountUM.Content ?: return + val selectedProvider = amountUM.selectedQuote.provider ?: return + val cryptoCurrency = params.secondaryCryptoCurrency ?: return + + analyticsEventHandler.send( + SwapAmountAnalyticEvents.ProviderSelectorClicked( + categoryName = params.analyticsCategoryName, + ), + ) + + bottomSheetNavigation.activate( + SwapChooseProviderConfig( + providers = amountUM.swapQuotes, + cryptoCurrency = cryptoCurrency, + selectedProvider = selectedProvider, + userCountry = userCountry, + ), + ) + } + private fun confirmSendWithSwapClose() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data @@ -252,6 +333,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, + showBestRateAnimation = showBestRateAnimation, ), ) } @@ -285,6 +367,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, + showBestRateAnimation = showBestRateAnimation, ), ) } @@ -392,6 +475,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this@SwapAmountModel, isBalanceHidden = params.isBalanceHidingFlow.value, + showBestRateAnimation = showBestRateAnimation, ), ) startLoadingQuotesTask(isSilentReload = false) @@ -448,9 +532,12 @@ internal class SwapAmountModel @Inject constructor( SwapDirection.Reverse -> state.secondaryAmount.amountField } as? AmountState.Data - if (fromAmount?.amountTextField?.isError == true) return + val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() - val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value ?: return + if (fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero()) { + uiState.transformerUpdate(SwapQuoteEmptyStateTransformer) + return + } val swapGroups = state.swapCurrencies.getGroupWithDirection(state.swapDirection) @@ -501,6 +588,7 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, + needApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), ), ) @@ -574,7 +662,7 @@ internal class SwapAmountModel @Inject constructor( ).onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( - title = resourceReference(R.string.common_swap), + title = resourceReference(R.string.common_amount), subtitle = null, backIconRes = if (route.isEditMode) { R.drawable.ic_back_24 @@ -590,7 +678,7 @@ internal class SwapAmountModel @Inject constructor( }, isEnabled = state.isPrimaryButtonEnabled, onClick = { - saveResult() + onAmountNext() params.callback.onNextClick() }, ), @@ -600,7 +688,7 @@ internal class SwapAmountModel @Inject constructor( } private companion object { - const val DEBOUNCE_AMOUNT_DELAY = 1000L + const val DEBOUNCE_AMOUNT_DELAY = 500L const val QUOTES_UPDATE_DELAY = 10000L } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index c4869a1825..fe05161fec 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -4,12 +4,13 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.utils.extensions.isZero +import com.tangem.utils.isNullOrZero import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.min @@ -32,12 +33,13 @@ internal object SwapAmountQuoteUtils { secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate } + val isRatesNull = fromRate.isNullOrZero() || toRate.isNullOrZero() + val isAmountNull = fromTokenAmount.isZero() || toTokenAmount.isZero() + if (isRatesNull || isAmountNull) return null + val fromTokenFiatValue = fromTokenAmount.multiply(fromRate) val toTokenFiatValue = toTokenAmount.multiply(toRate) - // Check for zero division - if (fromTokenFiatValue.isZero() || toTokenFiatValue.isZero()) return null - val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf { @@ -56,15 +58,20 @@ internal object SwapAmountQuoteUtils { ): SwapAmountUM { if (this !is SwapAmountUM.Content) return this - return if ( + val updatedAmountField = if ( selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct ) { val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this - copy(primaryAmount = amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus)) + amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus) } else { if (secondaryCryptoCurrencyStatus == null) return this val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this - copy(secondaryAmount = amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus)) + amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus) } + + return copy( + isPrimaryButtonEnabled = updatedAmountField.amountField.isPrimaryButtonEnabled, + primaryAmount = updatedAmountField, + ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 8745ee371d..fd83068f8f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -8,14 +8,16 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.utils.StringsSigns.DOT internal class SwapAmountFieldConverter( private val swapDirection: SwapDirection, @@ -60,15 +62,26 @@ internal class SwapAmountFieldConverter( } private fun getSubtitle(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> resourceReference( - R.string.common_balance, - wrappedList( + selectedType.isEnteringField() -> combinedReference( + stringReference( cryptoCurrencyStatus.value.amount.format { crypto(cryptoCurrency = cryptoCurrencyStatus.currency) }, ), + stringReference(value = " $DOT "), + stringReference( + cryptoCurrencyStatus.value.fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), ).orMaskWithStars(isBalanceHidden) - selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_amount_text) + selectedType.isViewingField() -> resourceReference( + R.string.send_with_swap_recipient_get_amount, + + ) else -> TextReference.Companion.EMPTY } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index f5738cbd5e..c014e539e7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -57,6 +57,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } else { @@ -68,6 +69,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index 2b25b3314b..3279e896ab 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -3,10 +3,10 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM @@ -23,6 +23,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, + private val showBestRateAnimation: Boolean, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( @@ -52,6 +53,8 @@ internal class SwapAmountPrimaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, + showBestRateAnimation = showBestRateAnimation, + showFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index c4ba35f561..7a0a94e535 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -3,10 +3,10 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter @@ -24,6 +24,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, + private val showBestRateAnimation: Boolean, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( @@ -51,6 +52,8 @@ internal class SwapAmountSecondaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, + showBestRateAnimation = showBestRateAnimation, + showFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 7213bcb3c5..cd0df36880 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer @@ -18,6 +19,7 @@ internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, + private val needApplyFCARestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -29,6 +31,7 @@ internal class SwapAmountSelectQuoteTransformer( return prevState.copy( isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, selectedQuote = quoteUM, + showFCAWarning = needApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content val amountField = swapAmountField?.amountField as? AmountState.Data diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index e6b6c96434..adfcc37a91 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -8,8 +8,10 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.StringsSigns import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -20,22 +22,33 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, + private val needApplyFcaRestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + val isSingleProvider = quotes.filter { + it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || + (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + }.isSingleItem() + val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty - val selectedQuote = if (isSilentReload) { + val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { prevState.selectedQuote } else { - bestQuote + (bestQuote as? SwapQuoteUM.Content)?.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) ?: bestQuote } val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + needApplyFCARestrictions = needApplyFcaRestrictions && + selectedQuote.provider?.isRestrictedByFCA() == true, ) val updatedState = selectQuoteTransformer.transform(prevState = prevState) @@ -43,21 +56,29 @@ internal class SwapAmountSetQuotesTransformer( return updatedState.copy( isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote), + swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), ) } - private fun getQuotesWithDiff(sortedQuotes: List, bestQuote: SwapQuoteUM): ImmutableList { + private fun getQuotesWithDiff( + sortedQuotes: List, + bestQuote: SwapQuoteUM, + isSingleProvider: Boolean, + ): ImmutableList { return sortedQuotes.sortedWith(SwapQuotesComparator) .map { quote -> if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy(diffPercent = DifferencePercent.Best) + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { // current / selected - 1 val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE quote.copy( diffPercent = DifferencePercent.Diff( + isPositive = percent.isPositive(), percent = stringReference( if (percent.isPositive()) { "${StringsSigns.PLUS}${percent.format { percent() }}" diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt index dde5dd9c4c..5ed86c33d1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.utils.transformer.Transformer diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt index 5a7e9eccb3..c364fd36a0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -17,33 +17,39 @@ internal class SwapAmountValueChangeTransformer( override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - return prevState - .copy(selectedQuote = SwapQuoteUM.Loading) - .updateAmount( - onPrimaryAmount = { primaryStatus -> + val updatedState = prevState.updateAmount( + onPrimaryAmount = { primaryStatus -> + copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = primaryStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = value, + ).transform(prevState.primaryAmount.amountField), + ) + }, + onSecondaryAmount = { secondaryStatus -> + if (secondaryMaximumAmountBoundary != null) { copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = primaryStatus, - maxEnterAmount = primaryMaximumAmountBoundary, - minimumTransactionAmount = primaryMinimumAmountBoundary, + cryptoCurrencyStatus = secondaryStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, value = value, - ).transform(prevState.primaryAmount.amountField), + ).transform(prevState.secondaryAmount.amountField), ) - }, - onSecondaryAmount = { secondaryStatus -> - if (secondaryMaximumAmountBoundary != null) { - copy( - amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = secondaryStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = value, - ).transform(prevState.secondaryAmount.amountField), - ) - } else { - this - } - }, - ) + } else { + this + } + }, + ) + + return (updatedState as? SwapAmountUM.Content)?.copy( + selectedQuote = if (updatedState.isPrimaryButtonEnabled) { + SwapQuoteUM.Empty + } else { + SwapQuoteUM.Loading + }, + ) ?: updatedState } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt new file mode 100644 index 0000000000..32cd5ea158 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.transformer.Transformer + +internal object SwapQuoteEmptyStateTransformer : Transformer { + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + return prevState.copy( + selectedQuote = SwapQuoteUM.Empty, + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt index a26deaf753..b431b0472a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt @@ -16,6 +16,7 @@ internal object SwapQuoteLoadingStateTransformer : Transformer { } return prevState.copy( selectedQuote = SwapQuoteUM.Loading, + isPrimaryButtonEnabled = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index 291627de98..f7129e04bc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.ui.extensions.TextReference @@ -34,11 +36,13 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM -@Suppress("DestructuringDeclarationWithTooManyEntries") +@Suppress("DestructuringDeclarationWithTooManyEntries", "LongParameterList") @Composable internal fun SwapAmountBlockContent( amountUM: SwapAmountUM, @@ -46,6 +50,7 @@ internal fun SwapAmountBlockContent( onProviderSelectClick: () -> Unit, onInfoClick: () -> Unit, onClick: () -> Unit, + onFinishAnimation: () -> Unit, modifier: Modifier = Modifier, ) { if (amountUM !is SwapAmountUM.Content) return @@ -61,34 +66,11 @@ internal fun SwapAmountBlockContent( ), ) { val (from, to, separator, provider) = createRefs() - AmountBlockV2( - amountState = amountUM.primaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(from) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) - }, - ) - AmountBlockV2( - amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( - title = resourceReference(R.string.send_with_swap_recipient_amount_title), - availableBalance = TextReference.EMPTY, - ) ?: amountUM.secondaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(to) { - top.linkTo(from.bottom, 8.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick) - }, + SwapAmountBlock( + amountUM = amountUM, + fromAmountRef = from, + toAmountRef = to, + onInfoClick = onInfoClick, ) SwapAmountDivider( modifier = Modifier.constrainAs(separator) { @@ -98,43 +80,109 @@ internal fun SwapAmountBlockContent( end.linkTo(parent.end) }, ) + val quoteContent = amountUM.selectedQuote as? SwapQuoteUM.Content + val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( + isBestRate = isBestRate, + isSingleProvider = quoteContent?.isSingleProvider == true, + showBestRateAnimation = amountUM.showBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, + onFinishAnimation = onFinishAnimation, modifier = Modifier.constrainAs(provider) { top.linkTo(to.bottom) bottom.linkTo(parent.bottom) start.linkTo(parent.start) end.linkTo(parent.end) }, + showFCAWarning = amountUM.showFCAWarning, ) } } @Composable -private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) { +private fun ConstraintLayoutScope.SwapAmountBlock( + amountUM: SwapAmountUM.Content, + fromAmountRef: ConstrainedLayoutReference, + toAmountRef: ConstrainedLayoutReference, + onInfoClick: () -> Unit, +) { + AmountBlockV2( + amountState = amountUM.primaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(fromAmountRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.primaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) + AmountBlockV2( + amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( + title = resourceReference(R.string.send_with_swap_recipient_amount_title), + availableBalance = TextReference.EMPTY, + availableBalanceShort = TextReference.EMPTY, + ) ?: amountUM.secondaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(toAmountRef) { + top.linkTo(fromAmountRef.bottom, 8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.secondaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) +} + +@Composable +private fun SwapPriceImpact( + amountFieldUM: SwapAmountFieldUM, + selectedAmountType: SwapAmountType, + onInfoClick: () -> Unit, +) { + if (amountFieldUM.amountType == selectedAmountType) return + val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact + val iconColor = if (priceImpact != null) { + TangemTheme.colors.icon.attention + } else { + TangemTheme.colors.icon.informative + } + if (priceImpact != null) { Text( text = priceImpact.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.attention, ) - Icon( - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_information_24), - ), - tint = TangemTheme.colors.icon.attention, - contentDescription = null, - modifier = Modifier - .size(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = onInfoClick, - ), - ) } + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_information_24), + ), + tint = iconColor, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onInfoClick, + ), + ) } @Composable @@ -146,10 +194,7 @@ private fun SwapAmountDivider(modifier: Modifier = Modifier) { style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .background(TangemTheme.colors.stroke.primary, RoundedCornerShape(32.dp)) - .padding(1.dp) - .background(TangemTheme.colors.text.primary2, RoundedCornerShape(32.dp)) // workaround - .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f), RoundedCornerShape(32.dp)) + .background(TangemTheme.colors.button.secondary, RoundedCornerShape(32.dp)) .padding(horizontal = 11.dp, vertical = 5.dp) .align(Alignment.Center), ) @@ -195,6 +240,7 @@ private fun SwapAmountBlockContent_Preview() { onProviderSelectClick = {}, onInfoClick = {}, onClick = {}, + onFinishAnimation = {}, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index 1ac34a19e2..41957bf386 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -3,8 +3,6 @@ package com.tangem.features.swap.v2.impl.amount.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -26,7 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountFieldV2 -import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH2 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.atoms.text.EllipsisText @@ -38,7 +36,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.express.models.ExpressRateType -import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType @@ -54,13 +51,17 @@ internal fun SwapAmountContent( clickIntents: SwapAmountClickIntents, modifier: Modifier = Modifier, ) { + val swapAmountContent = amountUM as? SwapAmountUM.Content + val isFixedRate = swapAmountContent?.swapRateType == ExpressRateType.Fixed ConstraintLayout( modifier = modifier, ) { val (amountFromRef, amountToRef, middleButtonRef) = createRefs() SwapAmountBlock( - amountUM = amountUM, amountFieldUM = amountUM.primaryAmount, + selectedAmountType = amountUM.selectedAmountType, + selectedQuote = swapAmountContent?.selectedQuote, + isFixedRate = isFixedRate, clickIntents = clickIntents, modifier = Modifier.constrainAs(amountFromRef) { top.linkTo(parent.top) @@ -69,8 +70,10 @@ internal fun SwapAmountContent( }, ) SwapAmountBlock( - amountUM = amountUM, amountFieldUM = amountUM.secondaryAmount, + selectedAmountType = amountUM.selectedAmountType, + selectedQuote = swapAmountContent?.selectedQuote, + isFixedRate = isFixedRate, clickIntents = clickIntents, modifier = Modifier.constrainAs(amountToRef) { top.linkTo(amountFromRef.bottom, 8.dp) @@ -100,7 +103,7 @@ private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = M modifier = modifier .heightIn(max = 28.dp) .clip(RoundedCornerShape(32.dp)) - .background(TangemTheme.colors.background.secondary) + .background(TangemTheme.colors.button.secondary) .clickable(onClick = onClick) .padding(vertical = 6.dp, horizontal = 12.dp), ) { @@ -109,10 +112,6 @@ private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = M style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) - VerticalDivider( - thickness = 1.dp, - color = TangemTheme.colors.icon.inactive, - ) Icon( painter = rememberVectorPainter( ImageVector.vectorResource(R.drawable.ic_close_24), @@ -126,11 +125,14 @@ private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = M @Composable private fun SwapAmountBlock( - amountUM: SwapAmountUM, amountFieldUM: SwapAmountFieldUM, + selectedQuote: SwapQuoteUM?, + selectedAmountType: SwapAmountType, + isFixedRate: Boolean, clickIntents: SwapAmountClickIntents, modifier: Modifier = Modifier, ) { + val isSelectedAmountType = selectedAmountType == amountFieldUM.amountType Column( modifier = modifier .padding(horizontal = 16.dp) @@ -138,7 +140,7 @@ private fun SwapAmountBlock( .fillMaxWidth() .background(TangemTheme.colors.background.action), ) { - AnimatedVisibility(amountUM.selectedAmountType == amountFieldUM.amountType) { + AnimatedVisibility(isSelectedAmountType) { Box { SwapAmountEditBlock( amountFieldUM = amountFieldUM, @@ -157,8 +159,10 @@ private fun SwapAmountBlock( } } SwapAmountInfo( - amountUM = amountUM, amountFieldUM = amountFieldUM, + selectedQuote = selectedQuote, + isSelectedAmountType = isSelectedAmountType, + isFixedRate = isFixedRate, onExpandEditField = clickIntents::onExpandEditField, onSelectTokenClick = clickIntents::onSelectTokenClick, onMaxAmountClick = clickIntents::onMaxValueClick, @@ -187,7 +191,7 @@ private fun SwapAmountEditBlock( } else { Text( text = (amountFieldUM.amountField as AmountState.Data).title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) } @@ -201,10 +205,13 @@ private fun SwapAmountEditBlock( } } +@Suppress("LongParameterList") @Composable private fun SwapAmountInfo( - amountUM: SwapAmountUM, amountFieldUM: SwapAmountFieldUM, + selectedQuote: SwapQuoteUM?, + isSelectedAmountType: Boolean, + isFixedRate: Boolean, onExpandEditField: (SwapAmountType) -> Unit, onMaxAmountClick: () -> Unit, onSelectTokenClick: () -> Unit, @@ -220,7 +227,7 @@ private fun SwapAmountInfo( indication = ripple(), enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, onClick = { - if ((amountUM as? SwapAmountUM.Content)?.swapRateType == ExpressRateType.Fixed) { + if (isFixedRate) { onExpandEditField(amountFieldUM.amountType) } else { onSelectTokenClick() @@ -237,107 +244,85 @@ private fun SwapAmountInfo( bottom = 16.dp, ), ) - SwapAmountInfoMain(amountFieldUM = amountFieldUM) + SwapAmountInfoMain( + amountFieldUM = amountFieldUM, + selectedQuote = selectedQuote, + isSelectedAmountType = isSelectedAmountType, + ) SpacerWMax() AnimatedContent( - amountUM, - ) { wrappedAmountUM -> - if (wrappedAmountUM is SwapAmountUM.Content) { - SwapAmountInfoExtra( - amountUM = wrappedAmountUM, - amountFieldUM = amountFieldUM, - onMaxAmountClick = onMaxAmountClick, + targetState = isSelectedAmountType, + ) { isSelected -> + if (isSelected) { + AmountMaxButton(onMaxAmountClick) + } else { + SwapAmountInfoQuote( + quoteUM = selectedQuote, + isFixedRate = isFixedRate, onSelectTokenClick = onSelectTokenClick, ) - } else { - RectangleShimmer() } } } } @Composable -private fun SwapAmountInfoMain(amountFieldUM: SwapAmountFieldUM, modifier: Modifier = Modifier) { - AnimatedContent( - targetState = amountFieldUM is SwapAmountFieldUM.Content, +private fun SwapAmountInfoMain( + amountFieldUM: SwapAmountFieldUM, + selectedQuote: SwapQuoteUM?, + isSelectedAmountType: Boolean, + modifier: Modifier = Modifier, +) { + Column( modifier = modifier, - ) { isContent -> - if (isContent && amountFieldUM is SwapAmountFieldUM.Content) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { + ) { + AnimatedContent(amountFieldUM is SwapAmountFieldUM.Content) { isContent -> + if (isContent && amountFieldUM is SwapAmountFieldUM.Content) { Text( text = amountFieldUM.title.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, maxLines = 1, ) + } else { + TextShimmer( + style = TangemTheme.typography.subtitle2, + modifier = Modifier.width(56.dp), + ) + } + } + AnimatedContent(isSelectedAmountType) { isSelected -> + if (isSelected && amountFieldUM is SwapAmountFieldUM.Content) { + SpacerH2() EllipsisText( text = amountFieldUM.subtitle.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ellipsis = amountFieldUM.subtitleEllipsis, ) - } - } else { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.width(56.dp), - ) - TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), - ) - } - } - } -} - -@Composable -private fun SwapAmountInfoExtra( - amountUM: SwapAmountUM.Content, - amountFieldUM: SwapAmountFieldUM, - onMaxAmountClick: () -> Unit, - onSelectTokenClick: () -> Unit, -) { - when (amountFieldUM.amountType) { - SwapAmountType.From -> when (amountUM.swapDirection) { - SwapDirection.Direct -> { - AnimatedVisibility( - visible = amountUM.selectedAmountType == amountFieldUM.amountType, - enter = fadeIn(), - exit = fadeOut(), - ) { - AmountMaxButton(onMaxAmountClick) - } - } - SwapDirection.Reverse -> { - SwapAmountInfoQuote( - quoteUM = amountUM.selectedQuote, - swapRateType = amountUM.swapRateType, - onSelectTokenClick = onSelectTokenClick, - ) - } - } - - SwapAmountType.To -> when (amountUM.swapDirection) { - SwapDirection.Direct -> { - SwapAmountInfoQuote( - quoteUM = amountUM.selectedQuote, - swapRateType = amountUM.swapRateType, - onSelectTokenClick = onSelectTokenClick, - ) - } - SwapDirection.Reverse -> { - AnimatedVisibility( - visible = amountUM.selectedAmountType == amountFieldUM.amountType, - enter = fadeIn(), - exit = fadeOut(), - ) { - AmountMaxButton(onMaxAmountClick) + } else { + AnimatedContent(selectedQuote) { quote -> + when (quote) { + is SwapQuoteUM.Content -> { + SpacerH2() + EllipsisText( + text = stringResourceSafe( + R.string.send_with_swap_recipient_get_amount, + quote.quoteAmountValue.resolveReference(), + ), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + SwapQuoteUM.Loading -> { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + else -> Unit + } } } } @@ -353,7 +338,7 @@ private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { modifier = Modifier .padding(end = 16.dp) .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.background.secondary) + .background(TangemTheme.colors.button.secondary) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), @@ -364,11 +349,11 @@ private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { } @Composable -private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM, swapRateType: ExpressRateType, onSelectTokenClick: () -> Unit) { +private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM?, isFixedRate: Boolean, onSelectTokenClick: () -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.conditionalCompose( - condition = swapRateType == ExpressRateType.Fixed, + condition = isFixedRate, modifier = { clickable( interactionSource = remember { MutableInteractionSource() }, @@ -378,29 +363,20 @@ private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM, swapRateType: ExpressRateT }, ), ) { - when (quoteUM) { - is SwapQuoteUM.Content -> EllipsisText( - text = quoteUM.quoteAmountValue.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(end = 2.dp), - ) - - is SwapQuoteUM.Error, - is SwapQuoteUM.Empty, - -> Box(modifier = Modifier.padding(start = 16.dp)) - - is SwapQuoteUM.Loading -> CircularProgressIndicator( - color = TangemTheme.colors.icon.inactive, - modifier = Modifier - .padding(end = 4.dp) - .size(20.dp), - ) - - is SwapQuoteUM.Allowance -> Text( - text = "ALLOWANCE NOT IMPLEMENTED", - ) + AnimatedContent( + quoteUM, + ) { quote -> + when (quote) { + is SwapQuoteUM.Loading -> CircularProgressIndicator( + color = TangemTheme.colors.icon.inactive, + modifier = Modifier + .padding(end = 4.dp) + .size(20.dp), + ) + else -> Box(modifier = Modifier.padding(start = 16.dp)) + } } + Icon( painter = rememberVectorPainter( ImageVector.vectorResource(R.drawable.ic_chevron_24), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 1c60ad739a..4c55f4759a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -12,6 +12,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onSeparatorClick() {} + override fun onProviderClick() {} + override fun onAmountValueChange(value: String) {} override fun onAmountPasteTriggerDismiss() {} diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index b40ecc6c80..02419e0cd6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -9,10 +9,10 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM @@ -39,6 +39,7 @@ internal data object SwapAmountContentPreview { hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "Bitcoin", @@ -68,6 +69,7 @@ internal data object SwapAmountContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ā‰ˆ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) val emptyState = SwapAmountUM.Content( @@ -87,6 +89,8 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, appCurrency = AppCurrency.Default, + showBestRateAnimation = false, + showFCAWarning = false, ) val defaultState = SwapAmountUM.Content( @@ -123,5 +127,7 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, isPrimaryButtonEnabled = true, + showBestRateAnimation = false, + showFCAWarning = true, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt index 744a8a3c91..c04a88654c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.features.swap.v2.impl.chooseprovider.model.SwapChooseProviderModel import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderBottomSheet import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -41,7 +42,7 @@ internal class SwapChooseProviderComponent( SwapChooseProviderBottomSheet(config = bottomSheetConfig) { SwapChooseProviderContent( - providerList = state.value.providerList, + contentUM = state.value, onProviderClick = model::onProviderClick, ) } @@ -51,6 +52,7 @@ internal class SwapChooseProviderComponent( val cryptoCurrency: CryptoCurrency, val selectedProvider: ExpressProvider, val providers: ImmutableList, + val userCountry: UserCountry, val callback: ModelCallback, val onDismiss: () -> Unit, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt index ad5eb47e18..7be1b3c0c7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt @@ -7,10 +7,12 @@ import kotlinx.collections.immutable.ImmutableList internal data class SwapChooseProviderBottomSheetContent( val providerList: ImmutableList, + val isApplyFCARestrictions: Boolean, val selectedProvider: ExpressProvider, ) internal data class SwapProviderListItem( val providerUM: ProviderChooseUM, + val swapProviderState: SwapProviderState, val quote: SwapQuoteUM, ) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt new file mode 100644 index 0000000000..24e18c0fb1 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt @@ -0,0 +1,34 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent + +@Deprecated("Use ProviderChooseUM with new design") +@Immutable +internal sealed class SwapProviderState { + + abstract val isSelected: Boolean + + data object Empty : SwapProviderState() { + override val isSelected = false + } + + data class Content( + override val isSelected: Boolean, + val name: String, + val type: String, + val iconUrl: String, + val subtitle: TextReference, + val additionalBadge: AdditionalBadge, + val diffPercent: DifferencePercent, + ) : SwapProviderState() + + @Immutable + sealed class AdditionalBadge { + data object FCAWarningList : AdditionalBadge() + data object BestTrade : AdditionalBadge() + data object Empty : AdditionalBadge() + data object PermissionRequired : AdditionalBadge() + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index 5b9ce17796..08511759a0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -3,11 +3,15 @@ package com.tangem.features.swap.v2.impl.chooseprovider.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent import com.tangem.features.swap.v2.impl.chooseprovider.model.converter.SwapProviderListItemConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.isSingleItem import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,10 +25,14 @@ internal class SwapChooseProviderModel @Inject constructor( private val params: SwapChooseProviderComponent.Params = paramsContainer.require() + private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() + private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) { SwapProviderListItemConverter( cryptoCurrency = params.cryptoCurrency, selectedProvider = params.selectedProvider, + needApplyFCARestrictions = needApplyFCARestrictions, + needBestRateBadge = params.providers.filterIsInstance().isSingleItem().not(), ) } @@ -37,8 +45,14 @@ internal class SwapChooseProviderModel @Inject constructor( } private fun getInitialState(): SwapChooseProviderBottomSheetContent { + val filteredProviderList = params.providers.filter { + it is SwapQuoteUM.Content || + it is SwapQuoteUM.Allowance || + (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + } return SwapChooseProviderBottomSheetContent( - providerList = swapProviderListItemConverter.convertList(params.providers) + isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), + providerList = swapProviderListItemConverter.convertList(filteredProviderList) .filterNotNull() .toPersistentList(), selectedProvider = params.selectedProvider, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 75234f816b..045d882f8f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -15,16 +15,28 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.converter.Converter internal class SwapProviderListItemConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, + private val needApplyFCARestrictions: Boolean, + needBestRateBadge: Boolean, ) : Converter { + + private val providerStateConverter = SwapProviderStateConverter( + cryptoCurrency = cryptoCurrency, + selectedProvider = selectedProvider, + needApplyFCARestrictions = needApplyFCARestrictions, + isNeedBestRateBadge = needBestRateBadge, + ) + override fun convert(value: SwapQuoteUM): SwapProviderListItem? { val provider = value.provider ?: return null return SwapProviderListItem( + swapProviderState = providerStateConverter.convert(value), providerUM = ProviderChooseUM( title = stringReference(provider.name), subtitle = stringReference(provider.type.typeName), @@ -66,9 +78,15 @@ internal class SwapProviderListItemConverter( }, ) } + is SwapQuoteUM.Content -> if (needApplyFCARestrictions && value.provider.isRestrictedByFCA()) { + ProviderChooseUM.ExtraUM.Action( + text = resourceReference(R.string.express_provider_fca_warning_list), + ) + } else { + ProviderChooseUM.ExtraUM.Empty + } SwapQuoteUM.Empty, SwapQuoteUM.Loading, - is SwapQuoteUM.Content, -> ProviderChooseUM.ExtraUM.Empty } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt new file mode 100644 index 0000000000..4a7a127735 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.model.converter + +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.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState.AdditionalBadge +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA +import com.tangem.utils.converter.Converter + +@Deprecated("Remove with new design") +internal class SwapProviderStateConverter( + private val cryptoCurrency: CryptoCurrency, + private val selectedProvider: ExpressProvider, + private val isNeedBestRateBadge: Boolean, + private val needApplyFCARestrictions: Boolean, +) : Converter { + + override fun convert(value: SwapQuoteUM): SwapProviderState { + return when (value) { + is SwapQuoteUM.Content -> value.convertToContent() + is SwapQuoteUM.Error -> value.convertToErrorContent() + is SwapQuoteUM.Allowance, + SwapQuoteUM.Empty, + SwapQuoteUM.Loading, + -> SwapProviderState.Empty + } + } + + private fun SwapQuoteUM.Content.convertToContent(): SwapProviderState { + val isBestRate = when (diffPercent) { + SwapQuoteUM.Content.DifferencePercent.Best -> true + else -> false + } + + val additionalBadge = when { + needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> AdditionalBadge.BestTrade + else -> AdditionalBadge.Empty + } + + return SwapProviderState.Content( + name = provider.name, + iconUrl = provider.imageLarge, + type = provider.type.typeName, + subtitle = quoteAmountValue, + additionalBadge = additionalBadge, + diffPercent = diffPercent, + isSelected = provider == selectedProvider, + ) + } + + private fun SwapQuoteUM.Error.convertToErrorContent(): SwapProviderState { + val additionalBadge = when { + needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + else -> AdditionalBadge.Empty + } + + return SwapProviderState.Content( + name = provider.name, + iconUrl = provider.imageLarge, + type = provider.type.typeName, + subtitle = when (val error = expressError) { + is ExpressError.AmountError.TooSmallError -> resourceReference( + id = R.string.express_provider_min_amount, + formatArgs = wrappedList( + error.amount.format { crypto(cryptoCurrency) }, + ), + ) + is ExpressError.AmountError.NotEnoughAllowanceError, + is ExpressError.AmountError.TooBigError, + -> resourceReference( + id = R.string.express_provider_max_amount, + formatArgs = wrappedList( + error.amount.format { crypto(cryptoCurrency) }, + ), + ) + else -> TextReference.EMPTY + }, + additionalBadge = additionalBadge, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSelected = false, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 46ab92931b..77f8f0cbef 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -1,6 +1,8 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -8,6 +10,7 @@ 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.alpha import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -15,22 +18,26 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.components.provider.ProviderChooseCrypto +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent -import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem import com.tangem.features.swap.v2.impl.chooseprovider.ui.preview.SwapChooseProviderContentPreview import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM -import kotlinx.collections.immutable.ImmutableList +import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM + +private const val DISABLED_COLORS_ALPHA = 0.5f @Composable internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { @@ -50,26 +57,45 @@ internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, cont @Composable internal fun SwapChooseProviderContent( - providerList: ImmutableList, + contentUM: SwapChooseProviderBottomSheetContent, onProviderClick: (SwapQuoteUM) -> Unit, modifier: Modifier = Modifier, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 13.dp), + modifier = modifier.padding(horizontal = 12.dp), ) { Text( text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 4.dp), ) - providerList.fastForEachIndexed { index, provider -> - ProviderChooseCrypto( - providerChooseUM = provider.providerUM, - onClick = { onProviderClick(provider.quote) }, - modifier = modifier - .conditional(index == 0) { padding(top = 24.dp) }, + AnimatedVisibility( + modifier = Modifier.padding(top = 12.dp), + visible = contentUM.isApplyFCARestrictions, + ) { + Notification( + config = SwapNotificationUM.Error.FCAWarningList.config, + containerColor = TangemTheme.colors.button.disabled, + iconTint = TangemTheme.colors.icon.warning, + ) + } + SpacerH12() + contentUM.providerList.fastForEachIndexed { index, provider -> + SwapProviderItem( + state = provider.swapProviderState, + modifier = Modifier + .selectedBorder(isSelected = provider.swapProviderState.isSelected) + .clickable( + enabled = provider.quote !is SwapQuoteUM.Error, + onClick = { onProviderClick(provider.quote) }, + ) + .padding(12.dp) + .conditional(provider.providerUM.extraUM is ProviderChooseUM.ExtraUM.Error) { + Modifier.alpha(DISABLED_COLORS_ALPHA) + }, ) } Icon( @@ -114,7 +140,11 @@ private fun SwapChooseProviderContent_Preview( ), ) { SwapChooseProviderContent( - providerList = params.providerList, + contentUM = SwapChooseProviderBottomSheetContent( + providerList = params.providerList, + isApplyFCARestrictions = true, + selectedProvider = SwapChooseProviderContentPreview.provider1, + ), onProviderClick = {}, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 43c2d3c8f0..a853552187 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -1,20 +1,20 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,11 +22,19 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstrainedLayoutReference +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope +import androidx.constraintlayout.compose.Visibility import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette @@ -36,14 +44,26 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.R +import kotlinx.coroutines.delay +@Suppress("LongParameterList") @Composable -fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> Unit, modifier: Modifier = Modifier) { +fun SwapChooseProviderContent( + expressProvider: ExpressProvider?, + isSingleProvider: Boolean, + isBestRate: Boolean, + showBestRateAnimation: Boolean, + onClick: () -> Unit, + onFinishAnimation: () -> Unit, + showFCAWarning: Boolean, + modifier: Modifier = Modifier, +) { Column( modifier = modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, + enabled = !isSingleProvider, ), ) { HorizontalDivider( @@ -51,54 +71,256 @@ fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> color = TangemTheme.colors.stroke.primary, modifier = Modifier.padding(horizontal = 12.dp), ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.padding(12.dp), - ) { + Row { Icon( painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_exchange_horizontal_24), + ImageVector.vectorResource(R.drawable.ic_stack_new_24), ), tint = TangemTheme.colors.icon.accent, contentDescription = null, + modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 12.dp), ) Text( text = stringResourceSafe(R.string.express_provider), - style = TangemTheme.typography.body2, + style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 12.dp), ) SpacerWMax() - SubcomposeAsyncImage( - modifier = modifier - .size(20.dp) - .clip(RoundedCornerShape(4.dp)) - .background(TangemColorPalette.Light1), - model = ImageRequest.Builder(context = LocalContext.current) - .data(expressProvider?.imageLarge) - .crossfade(enable = true) - .allowHardware(false) - .build(), - contentDescription = null, + ProviderInfo( + expressProvider = expressProvider, + isBestRate = isBestRate, + isSingleProvider = isSingleProvider, + showBestRateAnimation = showBestRateAnimation, + onFinishAnimation = onFinishAnimation, ) - Text( - text = expressProvider?.name.orEmpty(), // todo provider error - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(start = 6.dp), - ) - Icon( - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_chevron_24), - ), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - modifier = Modifier.padding(start = 4.dp), + } + if (showFCAWarning) { + FcaProviderWarning( + modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 12.dp), ) } } } +@Composable +private fun FcaProviderWarning(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_warning_16), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerW8() + Text( + text = stringResourceSafe(R.string.express_provider_in_fca_warning_list), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun ProviderInfo( + expressProvider: ExpressProvider?, + isBestRate: Boolean, + isSingleProvider: Boolean, + showBestRateAnimation: Boolean, + onFinishAnimation: () -> Unit, + modifier: Modifier = Modifier, +) { + ConstraintLayout(modifier = modifier) { + val (imageRef, nameRef, iconRef) = createRefs() + SubcomposeAsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(expressProvider?.imageLarge) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = 4.dp) }, + error = { + Box( + modifier = Modifier.background( + color = TangemColorPalette.Light1, + shape = RoundedCornerShape(4.dp), + ), + ) + }, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clip(RoundedCornerShape(4.dp)) + .constrainAs(imageRef) { + start.linkTo(parent.start) + top.linkTo(parent.top, 14.dp) + bottom.linkTo(parent.bottom, 14.dp) + }, + ) + Text( + text = expressProvider?.name.orEmpty(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(start = 6.dp) + .constrainAs(nameRef) { + start.linkTo(imageRef.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(iconRef.start, goneMargin = 12.dp) + }, + ) + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_select_18_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier + .padding(start = 4.dp) + .constrainAs(iconRef) { + start.linkTo(nameRef.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(parent.end, margin = 12.dp) + visibility = if (isSingleProvider) Visibility.Gone else Visibility.Visible + }, + ) + BestRateBadge( + showBestRateAnimation = showBestRateAnimation, + isBestRate = isBestRate && !isSingleProvider, + ref = imageRef, + onFinishAnimation = onFinishAnimation, + ) + } +} + +@Suppress("MagicNumber", "LongMethod") +@Composable +private fun ConstraintLayoutScope.BestRateBadge( + showBestRateAnimation: Boolean, + isBestRate: Boolean, + ref: ConstrainedLayoutReference, + onFinishAnimation: () -> Unit, + modifier: Modifier = Modifier, +) { + val animateState = remember { MutableTransitionState(false) } + val animationSpec = rememberBestRateAnimationSpec(animateState) + + LaunchedEffect(showBestRateAnimation) { + if (showBestRateAnimation) { + delay(600L) + animateState.targetState = true + delay(1_500L) + animateState.targetState = false + onFinishAnimation() + } + } + + val iconSize by animateDpAsState( + label = "iconSize", + animationSpec = animationSpec, + targetValue = if (animateState.targetState) { + 12.dp + } else { + 8.dp + }, + ) + val iconVerticalPaddings by animateDpAsState( + label = "iconVerticalPaddings", + animationSpec = animationSpec, + targetValue = if (animateState.targetState) { + 3.dp + } else { + 2.dp + }, + ) + val iconHorizontalPaddings by animateDpAsState( + label = "iconHorizontalPaddings", + animationSpec = animationSpec, + targetValue = if (animateState.targetState) { + 4.dp + } else { + 2.dp + }, + ) + + val startMargin by animateDpAsState( + label = "startMargin", + animationSpec = animationSpec, + targetValue = if (animateState.targetState) { + (-12).dp + } else { + (-10).dp + }, + ) + + val topMargin by animateDpAsState( + label = "topMargin", + animationSpec = animationSpec, + targetValue = if (animateState.targetState) { + (-12).dp + } else { + (-9).dp + }, + ) + + Row( + modifier = modifier + .constrainAs(createRef()) { + start.linkTo(ref.end, startMargin) + top.linkTo(ref.bottom, topMargin) + visibility = if (isBestRate) Visibility.Visible else Visibility.Gone + } + .background(TangemTheme.colors.stroke.transparency, RoundedCornerShape(120.dp)) + .padding(1.5.dp) + .background(TangemTheme.colors.icon.accent, RoundedCornerShape(120.dp)), + ) { + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_rounded_star_24), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.constant, + modifier = Modifier + .padding(iconHorizontalPaddings, iconVerticalPaddings) + .size(iconSize), + ) + AnimatedVisibility( + visibleState = animateState, + enter = expandIn(animationSpec = tween(durationMillis = 400, easing = EaseInOutQuart)) + + fadeIn(animationSpec = tween(delayMillis = 50, easing = EaseInOutQuint)), + exit = shrinkOut(animationSpec = tween(durationMillis = 400, easing = EaseInOutQuint)) + + fadeOut(animationSpec = tween(delayMillis = 100, durationMillis = 200, easing = EaseInOutQuint)), + label = "textAnimation", + modifier = Modifier.padding(end = 6.dp), + ) { + Text( + text = stringResourceSafe(R.string.express_provider_best_rate), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.constantWhite, + ) + } + } +} + +@Composable +private fun rememberBestRateAnimationSpec(animateState: MutableTransitionState): TweenSpec = remember( + animateState, +) { + tween( + durationMillis = 400, + easing = if (animateState.targetState) { + EaseInOutQuart + } else { + EaseInOutQuint + }, + ) +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) @@ -109,6 +331,9 @@ private fun SwapChooseProviderContent_Preview() { modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) { SwapChooseProviderContent( + isSingleProvider = false, + isBestRate = true, + showBestRateAnimation = true, expressProvider = ExpressProvider( providerId = "changelly", rateTypes = listOf(ExpressRateType.Fixed), @@ -121,6 +346,8 @@ private fun SwapChooseProviderContent_Preview() { slippage = null, ), onClick = {}, + showFCAWarning = true, + onFinishAnimation = {}, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt new file mode 100644 index 0000000000..4b8df6d2b6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt @@ -0,0 +1,232 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM + +@Deprecated("Use ProviderChooseCrypto with new design") +@Composable +internal fun SwapProviderItem(state: SwapProviderState, modifier: Modifier = Modifier) { + when (state) { + is SwapProviderState.Content -> ProviderContentState( + state = state, + modifier = modifier, + ) + is SwapProviderState.Empty -> { /* no-op */ + } + } +} + +@Suppress("LongMethod") +@Composable +private fun ProviderContentState(state: SwapProviderState.Content, modifier: Modifier = Modifier) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = 40.dp) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl) + .crossfade(enable = true).allowHardware(false).build(), + loading = { RectangleShimmer(radius = 8.dp) }, + error = { + ErrorProviderIcon(Modifier.size(size = 40.dp)) + }, + contentDescription = null, + ) + + Column(modifier = Modifier.padding(start = 12.dp)) { + Row { + Text( + text = state.name, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.type, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = 4.dp), + ) + val badgeModifier = Modifier.padding(start = 4.dp) + when (state.additionalBadge) { + SwapProviderState.AdditionalBadge.FCAWarningList -> FCABadgeItem(badgeModifier) + SwapProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier) + SwapProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier) + SwapProviderState.AdditionalBadge.Empty -> Unit + } + } + Row( + modifier = Modifier.padding(top = 2.dp), + ) { + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + if (state.diffPercent is SwapQuoteUM.Content.DifferencePercent.Diff) { + val textColor = if (state.diffPercent.isPositive) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.text.warning + } + + Text( + text = state.diffPercent.percent.resolveReference(), + style = TangemTheme.typography.body2, + color = textColor, + modifier = Modifier.padding(start = 4.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } + } + } +} + +@Composable +private fun ErrorProviderIcon(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCorners8, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + contentDescription = null, + ) + } +} + +@Composable +private fun BestTradeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResourceSafe(R.string.express_provider_best_rate), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + modifier = Modifier.padding(horizontal = 6.dp), + maxLines = 1, + ) + } +} + +@Composable +private fun PermissionBadgeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResourceSafe(id = R.string.express_provider_permission_needed), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = 6.dp), + maxLines = 1, + ) + } +} + +@Composable +private fun FCABadgeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResourceSafe(id = R.string.express_provider_fca_warning_list), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = 6.dp), + maxLines = 1, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProviderItemPreview( + @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, +) { + TangemThemePreview { + SwapProviderItem( + modifier = Modifier.background(TangemTheme.colors.background.action), + state = state.first, + ) + } +} + +private class ProviderItemParameterProvider : CollectionPreviewParameterProvider>( + collection = buildList { + val contentState = SwapProviderState.Content( + name = "1inch", + type = "DEX", + iconUrl = "", + subtitle = stringReference(value = "0,64554846 DAI ā‰ˆ 1 MATIC"), + additionalBadge = SwapProviderState.AdditionalBadge.Empty, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Diff( + isPositive = false, + percent = stringReference("-10%"), + ), + isSelected = true, + ) + val contentState2 = contentState.copy( + subtitle = stringReference(value = "1 132,46 MATIC"), + additionalBadge = SwapProviderState.AdditionalBadge.PermissionRequired, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Diff( + isPositive = true, + percent = stringReference("+10%"), + ), + ) + add(contentState to true) + add(contentState to false) + + add(contentState2 to true) + add(contentState2 to false) + }, +) +// endregion Preview \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index b96c7acb4b..238cf49cb3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -10,13 +10,14 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal internal object SwapChooseProviderContentPreview { - private val provider1 = ExpressProvider( + val provider1 = ExpressProvider( providerId = "changenow", rateTypes = listOf(ExpressRateType.Float), name = "ChangeNow", @@ -38,6 +39,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ā‰ˆ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) private val quote2 = SwapQuoteUM.Content( @@ -46,6 +48,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("13.12"), rate = stringReference("1 USD ā‰ˆ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSingleProvider = false, ) val state = SwapChooseProviderBottomSheetContent( @@ -65,6 +68,15 @@ internal object SwapChooseProviderContentPreview { ), ), ), + swapProviderState = SwapProviderState.Content( + name = provider1.name, + type = provider1.type.typeName, + iconUrl = "", + subtitle = stringReference("1800 POL"), + additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSelected = true, + ), quote = quote1, ), SwapProviderListItem( @@ -85,8 +97,18 @@ internal object SwapChooseProviderContentPreview { ), ), quote = quote2, + swapProviderState = SwapProviderState.Content( + name = provider2.name, + type = provider2.type.typeName, + iconUrl = "", + subtitle = stringReference("1800 POL"), + additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSelected = false, + ), ), ), selectedProvider = provider1, + isApplyFCARestrictions = false, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt index 70433b8957..c7605a26cc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt @@ -3,7 +3,6 @@ package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.features.swap.v2.impl.R @@ -26,22 +25,19 @@ internal class SwapChooseTokenAlertFactory @Inject constructor( } fun showChangeTokenAlert(onConfirm: () -> Unit, onDismiss: () -> Unit) { - // todo fix localization [REDACTED_TASK_KEY] uiMessageSender.send( DialogMessage( - title = stringReference("Changing token"), - message = stringReference( - "Are you sure you want to change the token? After changing, previous data will be reset.", - ), + title = resourceReference(R.string.send_with_swap_change_token_alert_title), + message = resourceReference(R.string.send_with_swap_change_token_alert_message), firstActionBuilder = { EventMessageAction( - title = stringReference("Change"), + title = resourceReference(R.string.common_change), onClick = onConfirm, ) }, secondActionBuilder = { EventMessageAction( - title = stringReference("Cancel"), + title = resourceReference(R.string.common_cancel), onClick = onDismiss, ) }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index 496e806384..45659420c7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -8,8 +9,11 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapTxType import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM @@ -26,6 +30,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class SwapChooseTokenNetworkModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -34,6 +39,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val swapChooseTokenAlertFactory: SwapChooseTokenAlertFactory, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require() @@ -79,6 +85,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( initialCurrency = params.initialCurrency, cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency, filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES, + swapTxType = SwapTxType.SendWithSwap, ).getOrElse { Timber.e(it.toString()) uiState.update( @@ -103,6 +110,23 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( private fun onSwapTokenClick(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency) { val prevSelectedCurrency = params.selectedCurrency?.network + analyticsEventHandler.send( + CommonSendAnalyticEvents.TokenChosen( + categoryName = params.analyticsCategoryName, + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + if (params.isSearchedToken) { + analyticsEventHandler.send( + CommonManageTokensAnalyticEvents.TokenSearched( + categoryName = params.analyticsCategoryName, + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + isTokenChosen = true, + ), + ) + } if (prevSelectedCurrency == null || cryptoCurrency.network == prevSelectedCurrency) { params.onResult(swapCurrencies, cryptoCurrency) } else { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index 03d5b2f8c9..555acd6d00 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -24,12 +24,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -44,7 +45,7 @@ import kotlinx.collections.immutable.toPersistentList internal fun SwapChooseTokenNetworkBottomSheet(config: TangemBottomSheetConfig) { TangemModalBottomSheet( config = config, - containerColor = TangemTheme.colors.background.tertiary, + containerColor = TangemTheme.colors.background.primary, title = { AnimatedContent( targetState = config.content is SwapChooseTokenNetworkContentUM.Content, @@ -94,22 +95,31 @@ internal fun SwapChooseTokenNetworkContent(state: SwapChooseTokenNetworkContentU @Composable private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList) { - Column { - swapNetworks.fastForEach { network -> + Column( + modifier = Modifier.padding( + top = 8.dp, + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + swapNetworks.fastForEachIndexed { index, network -> Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier .fillMaxWidth() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = swapNetworks.lastIndex, + addDefaultPadding = false, + ) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = { network.onNetworkClick() }, ) - .padding( - vertical = 12.dp, - horizontal = 14.dp, - ), + .padding(vertical = 12.dp, horizontal = 14.dp), ) { Image( modifier = Modifier.size(36.dp), @@ -143,10 +153,7 @@ private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList, - ) : ConfirmUM() + val tosUM: TosUM?, + ) : ConfirmUM() { + data class TosUM( + val tosLink: LegalUM?, + val policyLink: LegalUM?, + ) + + data class LegalUM( + val title: TextReference, + val link: String, + ) + } data class Success( override val isPrimaryButtonEnabled: Boolean = true, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index ec6aaf74e7..c8b7e6f038 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -34,12 +34,14 @@ internal sealed class SwapQuoteUM { val quoteAmount: BigDecimal, val quoteAmountValue: TextReference, val diffPercent: DifferencePercent, + val isSingleProvider: Boolean, val rate: TextReference, ) : SwapQuoteUM() { sealed class DifferencePercent { data object Empty : DifferencePercent() data object Best : DifferencePercent() data class Diff( + val isPositive: Boolean, val percent: TextReference, ) : DifferencePercent() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 986c936162..bc241e0ec0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -11,6 +11,8 @@ import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.ObserveLifecycleMode import com.arkivanov.decompose.value.subscribe +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -20,6 +22,7 @@ import com.tangem.core.ui.decompose.getEmptyComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.swap.models.R import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -43,6 +46,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( @Assisted private val params: SendWithSwapComponent.Params, private val sendDestinationComponentFactory: SendDestinationComponent.Factory, private val confirmComponentFactory: SendWithSwapConfirmComponent.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, ) : SendWithSwapComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -79,15 +83,23 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SwapAmountComponent -> { - // todo send with swap analytics + analyticsEventHandler.send( + CommonSendAnalyticEvents.AmountScreenOpened(categoryName = model.analyticCategoryName), + ) activeComponent.updateState(model.uiState.value.amountUM) } is SendDestinationComponent -> { - // todo send with swap analytics + analyticsEventHandler.send( + CommonSendAnalyticEvents.AddressScreenOpened(categoryName = model.analyticCategoryName), + ) activeComponent.updateState(model.uiState.value.destinationUM) } is SendWithSwapConfirmComponent -> if (model.currentRoute.value.isEditMode) { - // todo send with swap analytics + analyticsEventHandler.send( + CommonSendAnalyticEvents.ConfirmationScreenOpened( + categoryName = model.analyticCategoryName, + ), + ) activeComponent.updateState(model.uiState.value) } } @@ -101,7 +113,11 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( val stackState by childStack.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = ::onChildBack) + BackHandler( + onBack = { + (state.navigationUM as? NavigationUM.Content)?.backIconClick() ?: onChildBack() + }, + ) SendWithSwapContent(navigationUM = state.navigationUM, stackState = stackState) } @@ -120,7 +136,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( title = resourceReference(R.string.common_send), currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow, secondaryCryptoCurrency = null, swapDirection = SwapDirection.Direct, @@ -143,8 +159,8 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( state = model.uiState.value.destinationUM, currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = "", - title = resourceReference(R.string.send_recipient_label), + analyticsCategoryName = model.analyticCategoryName, + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, callback = model, @@ -162,7 +178,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( appCurrency = model.appCurrency, userWallet = model.userWallet, callback = model, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow, primaryFeePaidCurrencyStatusFlow = model.primaryFeePaidCurrencyStatusFlow, swapDirection = SwapDirection.Direct, @@ -177,7 +193,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( sendWithSwapUMFlow = model.uiState, currentRoute = model.currentRoute.filterIsInstance(), callback = model, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt new file mode 100644 index 0000000000..ad1927f7cd --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -0,0 +1,34 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER +import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents + +internal sealed class SendWithSwapAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { + + data class TransactionScreenOpened( + val providerName: String, + val feeType: AnalyticsParam.FeeType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Send With Swap In Progress Screen Opened", + params = mapOf( + PROVIDER to providerName, + "Commission" to if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast", + SEND_TOKEN to fromToken.symbol, + RECEIVE_TOKEN to toToken.symbol, + SEND_BLOCKCHAIN to fromToken.network.name, + RECEIVE_BLOCKCHAIN to toToken.network.name, + ), + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 1d02dcce0c..b0cfea80d2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -8,11 +8,13 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.entity.PredefinedValues @@ -41,6 +43,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( sendDestinationBlockComponent: SendDestinationBlockComponent.Factory, feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, sendNotificationsComponentFactory: SendNotificationsComponent.Factory, + private val urlOpener: UrlOpener, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: SendWithSwapConfirmModel = getOrCreateModel(params = params) @@ -87,6 +90,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( cryptoCurrencyStatus = model.primaryCurrencyStatus, feeStateConfiguration = FeeStateConfiguration.ExcludeLow, feeDisplaySource = FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, ), onResult = model::onFeeResult, ) @@ -101,7 +105,10 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( appCurrency = params.appCurrency, callback = model, notificationData = SendNotificationsComponent.Params.NotificationData( - destinationAddress = model.confirmData.enteredDestination.orEmpty(), + destinationAddress = when (val currency = model.primaryCurrencyStatus.currency) { + is CryptoCurrency.Token -> currency.contractAddress + is CryptoCurrency.Coin -> "0" + }, memo = null, amountValue = model.confirmData.enteredAmount.orZero(), reduceAmountBy = model.confirmData.reduceAmountBy.orZero(), @@ -151,6 +158,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( sendNotificationsUM = sendNotificationsUM, swapNotificationsComponent = swapNotificationsComponent, swapNotificationsUM = swapNotificationsUM, + onLinkClick = urlOpener::openUrl, modifier = modifier, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index d4a416b177..cebaaba9dd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -9,6 +9,9 @@ import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -16,15 +19,17 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM @@ -43,6 +48,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmInitialStateTransformer import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmationNotificationsTransformer @@ -73,6 +79,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val swapAlertFactory: SwapAlertFactory, private val appRouter: AppRouter, + private val analyticsEventHandler: AnalyticsEventHandler, swapTransactionSenderFactory: SwapTransactionSender.Factory, paramsContainer: ParamsContainer, ) : Model(), FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback { @@ -185,10 +192,22 @@ internal class SendWithSwapConfirmModel @Inject constructor( } fun showEditAmount() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Amount, + ), + ) router.push(SendWithSwapRoute.Amount(isEditMode = true)) } fun showEditDestination() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Address, + ), + ) router.push(SendWithSwapRoute.Destination(isEditMode = true)) } @@ -260,7 +279,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( txHash = txHash, networkId = primaryCurrencyStatus.currency.network.id, ).getOrNull().orEmpty() - + sendSuccessAnalytics() uiState.update { it.copy( confirmUM = ConfirmUM.Success( @@ -310,7 +329,10 @@ internal class SendWithSwapConfirmModel @Inject constructor( modelScope.launch { sendNotificationsUpdateTrigger.triggerUpdate( data = NotificationData( - destinationAddress = confirmData.enteredDestination.orEmpty(), + destinationAddress = when (val currency = primaryCurrencyStatus.currency) { + is CryptoCurrency.Token -> currency.contractAddress + is CryptoCurrency.Coin -> "0" + }, memo = null, amountValue = confirmData.enteredAmount.orZero(), reduceAmountBy = confirmData.reduceAmountBy, @@ -348,6 +370,33 @@ internal class SendWithSwapConfirmModel @Inject constructor( }.launchIn(modelScope) } + private fun sendSuccessAnalytics() { + val selectedProvider = confirmData.quote?.provider ?: return + val fromCurrency = confirmData.fromCryptoCurrencyStatus?.currency ?: return + val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return + val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return + val feeType = feeSelectorUM.toAnalyticType() + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.TransactionScreenOpened( + providerName = selectedProvider.name, + feeType = feeType, + fromToken = fromCurrency, + toToken = toCurrency, + ), + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.SendWithSwap( + blockchain = fromCurrency.network.name, + token = fromCurrency.symbol, + feeType = feeType, + ), + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + } + private fun configConfirmNavigation() { combine( flow = uiState, @@ -357,6 +406,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( it.second is SendWithSwapRoute.Confirm }.onEach { (state, _) -> val confirmUM = state.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isTransactionInProcess params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( @@ -367,6 +417,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( primaryButton = NavigationButton( textReference = resourceReference(R.string.common_send), iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isHapticClick = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, onClick = { when (confirmUM) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index ace6ba9272..458d1ebd4e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -5,12 +5,13 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDataModel import com.tangem.domain.swap.models.SwapDataTransactionModel +import com.tangem.domain.swap.models.SwapTxType import com.tangem.domain.swap.usecase.GetSwapDataUseCase import com.tangem.domain.swap.usecase.SwapTransactionSentUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase @@ -158,6 +159,7 @@ internal class SwapTransactionSender @AssistedInject constructor( provider = provider, txHash = txHash, timestamp = timestamp, + swapTxType = SwapTxType.SendWithSwap, ) onSendSuccess(txHash, timestamp, swapData) }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt index 4be5c0b56a..64e34d1673 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt @@ -15,6 +15,7 @@ internal class SendWithSwapConfirmInitialStateTransformer( showTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), + tosUM = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt index 014e57650d..3176e70a6c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -12,6 +13,8 @@ import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooHigh import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooLow +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM @@ -27,6 +30,7 @@ internal class SendWithSwapConfirmationNotificationsTransformer : Transformer, swapNotificationsComponent: SwapNotificationsComponent, swapNotificationsUM: ImmutableList, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Content Column(modifier = modifier) { LazyColumn( - modifier = Modifier.padding(horizontal = 12.dp), + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp), ) { item(key = "SendWithSwapBlocks") { Column( @@ -71,7 +78,80 @@ internal fun SendWithSwapConfirmContent( ) } } - SpacerHMax() - SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) + val sendFooter = confirmUM?.sendingFooter ?: TextReference.EMPTY + val legalFooter = getAnnotatedStringForLegals(confirmUM?.tosUM, onClick = onLinkClick) + SendingText( + footerText = if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) { + combinedReference(sendFooter, legalFooter) + } else { + TextReference.EMPTY + }, + ) + } +} + +@Composable +private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick: (String) -> Unit): TextReference { + if (tosUM == null) return TextReference.EMPTY + val tos = tosUM.tosLink + val policy = tosUM.policyLink + return if (tos != null && policy != null) { + val tosTitle = tos.title.resolveReference() + val policyTitle = policy.title.resolveReference() + val fullString = stringResourceSafe(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle) + val tosIndex = fullString.indexOf(tosTitle) + val policyIndex = fullString.indexOf(policyTitle) + + annotatedReference { + append(StringsSigns.POINT_SIGN) + appendSpace() + append(fullString.substring(0, tosIndex)) + withLink( + link = LinkAnnotation.Clickable( + tag = "TOS_TAG", + linkInteractionListener = { onClick(tos.link) }, + ), + block = { + appendColored( + text = fullString.substring(tosIndex, tosIndex + tosTitle.length), + color = TangemTheme.colors.text.accent, + ) + }, + ) + append(fullString.substring(tosIndex + tosTitle.length, policyIndex)) + withLink( + link = LinkAnnotation.Clickable( + tag = "POLICY_TAG", + linkInteractionListener = { onClick(policy.link) }, + ), + block = { + appendColored( + text = fullString.substring(policyIndex, policyIndex + policyTitle.length), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } + } else { + val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" } + val legalTitle = legal.title.resolveReference() + val fullString = stringResourceSafe(id = R.string.express_legal_one_placeholder, legalTitle) + val legalIndex = fullString.indexOf(legalTitle) + + annotatedReference { + append(fullString.substring(0, legalIndex)) + withLink( + link = LinkAnnotation.Clickable( + tag = "LEGAL_TAG", + linkInteractionListener = { onClick(legal.link) }, + ), + block = { + appendColored( + text = fullString.substring(legalIndex, legalIndex + legalTitle.length), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index d8a7e41bde..ede2387f9d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -13,14 +13,15 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM @@ -58,6 +59,7 @@ internal class SendWithSwapModel @Inject constructor( private val params: SendWithSwapComponent.Params = paramsContainer.require() + val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY val initialRoute = SendWithSwapRoute.Amount(false) val currentRoute = MutableStateFlow(initialRoute) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index e730666446..6e5406b9e6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -1,7 +1,6 @@ package com.tangem.features.swap.v2.impl.sendviaswap.success.ui import android.content.res.Configuration -import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -10,14 +9,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.transaction.Fee @@ -25,9 +19,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.InputRowBestRate @@ -41,7 +35,6 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters -import com.tangem.core.ui.utils.singleEvent import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType @@ -64,8 +57,6 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -private const val GRADIENT_ALPHA = 0.3f - @Composable internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { if (sendWithSwapUM.navigationUM !is NavigationUM.Content) return @@ -112,22 +103,16 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { FeeBlock(feeSelectorUM = feeSelectorUM) Spacer(Modifier.height(60.dp)) } - DoneButtons( - pairButtonsUM = sendWithSwapUM.navigationUM.secondaryPairButtonsUM, + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendWithSwapUM.navigationUM, modifier = Modifier .align(Alignment.BottomCenter) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - TangemTheme.colors.background.tertiary.copy(GRADIENT_ALPHA), - TangemTheme.colors.background.tertiary, - ), - ), - ) - .padding(top = 24.dp) - .padding(horizontal = 16.dp), - + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), ) } } @@ -217,16 +202,17 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) { .padding(TangemTheme.dimens.spacing12), ) { Text( - text = stringResourceSafe(com.tangem.common.ui.R.string.common_network_fee_title), + text = stringResourceSafe(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { - val feeAmount = feeSelectorUM.selectedFeeItem.fee.amount + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount SelectorRowItem( - titleRes = com.tangem.common.ui.R.string.common_fee_selector_option_market, - iconRes = com.tangem.common.ui.R.drawable.ic_bird_24, + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, preDot = stringReference( feeAmount.value.format { crypto( @@ -286,46 +272,6 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m } } -// TODO remove [REDACTED_TASK_KEY] -@Composable -private fun DoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = requireNotNull(leftButton.iconRes), - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = requireNotNull(rightButton.iconRes), - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} - // region Preview @Suppress("LongMethod") @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index 17355d4aa4..3075a24638 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -13,11 +13,9 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.plus import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton -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.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -57,20 +55,14 @@ internal fun SendWithSwapContent( ) { it.instance.Content(Modifier.weight(1f)) } - // TODO refactor [REDACTED_TASK_KEY] - val primaryButton = navigationUM.primaryButton - Row(modifier = Modifier.padding(16.dp)) { - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = primaryButton.onClick, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, + if (stackState.active.configuration != SendWithSwapRoute.Success) { + NavigationPrimaryButton( + navigationUM.primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), ) } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index a7faad5bca..96a200e419 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -25,12 +25,12 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError @@ -51,7 +51,7 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListManager: UserWalletsListManager, + private val userWalletsStore: UserWalletsStore, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, @@ -411,7 +411,7 @@ internal class DefaultSwapRepository( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - userWallet = requireNotNull(userWalletsListManager.selectedUserWalletSync), + userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull), ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index e5c25f522c..d4fec5e8ed 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -92,21 +92,37 @@ internal class DefaultSwapTransactionRepository( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ), ) { savedTransactions, txStatuses -> - val currencyTxs = savedTransactions?.filter { - it.userWalletId == userWallet.walletId.stringValue && - ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value - ) + + val currencyToTxs = savedTransactions?.filter { + val isUserWallet = it.userWalletId == userWallet.walletId.stringValue + val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && toCurrency } - currencyTxs?.mapNotNull { + val currencyFromTxs = savedTransactions?.filter { + val isUserWallet = it.userWalletId == userWallet.walletId.stringValue + val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && fromCurrency + } + + val toTxs = currencyToTxs?.mapNotNull { + converter.convertBack( + value = it, + userWallet = userWallet, + txStatuses = txStatuses, + onFilter = { it.swapTxTypeDTO == SwapTxTypeDTO.Swap }, + ) + }.orEmpty() + + val fromTxs = currencyFromTxs?.mapNotNull { converter.convertBack( value = it, userWallet = userWallet, txStatuses = txStatuses, ) - } + }.orEmpty() + + fromTxs + toTxs } .flowOn(dispatchers.default) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 4735f63fdc..abef06ede4 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -34,6 +34,7 @@ internal class SavedSwapTransactionListConverter( value: SavedSwapTransactionListModelInner, userWallet: UserWallet, txStatuses: Map, + onFilter: (SavedSwapTransactionModel) -> Boolean = { true }, ): SavedSwapTransactionListModel? { val fromToken = value.fromTokensResponse val toToken = value.toTokensResponse @@ -50,17 +51,19 @@ internal class SavedSwapTransactionListConverter( ) ?: return null return SavedSwapTransactionListModel( - transactions = value.transactions.map { tx -> - val status = txStatuses[tx.txId] - val refundCurrency = status?.refundTokensResponse?.let { id -> - responseCryptoCurrenciesFactory.createCurrency( - responseToken = id, - userWallet = userWallet, - ) - } - val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) - tx.copy(status = statusWithRefundCurrency) - }, + transactions = value.transactions + .filter(onFilter) + .map { tx -> + val status = txStatuses[tx.txId] + val refundCurrency = status?.refundTokensResponse?.let { id -> + responseCryptoCurrenciesFactory.createCurrency( + responseToken = id, + userWallet = userWallet, + ) + } + val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) + tx.copy(status = statusWithRefundCurrency) + }, userWalletId = value.userWalletId, fromCryptoCurrencyId = value.fromCryptoCurrencyId, toCryptoCurrencyId = value.toCryptoCurrencyId, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 8dbd0c2597..4aada993a6 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,9 +8,9 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.converters.ErrorsDataConverter @@ -34,7 +34,7 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, + userWalletsStore: UserWalletsStore, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, @@ -45,7 +45,7 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - userWalletsListManager = userWalletsListManager, + userWalletsStore = userWalletsStore, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index 68fea21eff..94742cbac9 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -50,4 +50,16 @@ data class SavedSwapTransactionModel( val provider: SwapProvider, @Json(name = "status") val status: ExchangeStatusModel? = null, -) \ No newline at end of file + @Json(name = "swapTxType") + val swapTxTypeDTO: SwapTxTypeDTO? = SwapTxTypeDTO.Swap, +) + +// TODO refactor to use separate models to store +@JsonClass(generateAdapter = false) +enum class SwapTxTypeDTO { + @Json(name = "Swap") + Swap, + + @Json(name = "SendWithSwap") + SendWithSwap, +} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index abbcf27ea2..bf97fe58d9 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.models.domain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import java.math.BigDecimal /** diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index be89782000..e8e10f7165 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt index 85fc60140b..b71ed02569 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt index 3c27003179..c62a113adc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 673cc0e7fc..3646a0d6b5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1e0209b1e3..de44a22010 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -19,6 +19,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet @@ -26,7 +27,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 5d0745ca74..ed1a7f3f50 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -61,7 +61,6 @@ dependencies { /** Compose */ implementation(deps.arrow.core) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 450234b855..86245f0585 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -3,11 +3,11 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency import com.tangem.feature.swap.models.SwapSelectTokenStateHolder @@ -111,10 +111,11 @@ class TokensDataConverter( } private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = cryptoCurrencyStatus.value.fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return cryptoCurrencyStatus.value.fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index d0b4f81377..2e8026f8db 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -29,7 +29,11 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -40,11 +44,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents @@ -1351,8 +1351,6 @@ internal class SwapModel @Inject constructor( val transaction = dataState.swapDataModel?.transaction val fromCurrencyStatus = dataState.fromCryptoCurrency ?: initialFromStatus val network = fromCurrencyStatus.currency.network - val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY] - .getOrElse { error("CardInfo must be not null") } saveBlockchainErrorUseCase( error = BlockchainErrorInfo( @@ -1366,6 +1364,13 @@ internal class SwapModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + + val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) + .getOrElse { error("CardInfo must be not null") } + val email = FeedbackEmailType.SwapProblem( cardInfo = cardInfo, providerName = dataState.selectedProvider?.name.orEmpty(), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 69f4309c55..36018d5189 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt index 86a8a33195..604203a8f7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.model -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index a041840318..2364dbd0a6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 836783751f..3946ea9358 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -8,17 +8,19 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.FeeItemState @@ -89,7 +91,8 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore .padding( vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16, - ), + ) + .testTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT), style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start), onClick = click, ) @@ -107,7 +110,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), @@ -119,7 +122,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { } FeeType.PRIORITY -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_fast, + title = resourceReference(R.string.common_fee_selector_option_fast), iconRes = R.drawable.ic_hare_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 3c9d32501e..ac6ebfd1b3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -27,6 +28,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA import com.tangem.core.ui.utils.GrayscaleColorFilter import com.tangem.feature.swap.models.states.PercentDifference @@ -117,7 +119,9 @@ private fun ProviderContentState( ) Column( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .testTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK), ) { Row { if (state.namePrefix == ProviderState.PrefixType.PROVIDED_BY) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index ad13b81ea0..0de047350a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -13,13 +13,13 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -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.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.promo.models.StoryContent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError @@ -1273,7 +1273,12 @@ internal class StateBuilder( private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) + return amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 2e2d7ab136..ada4986f2f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -36,6 +37,7 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* @@ -275,7 +277,8 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { onClick = state.onChangeCardsClicked, indication = ripple(), interactionSource = remember { MutableInteractionSource() }, - ), + ) + .testTag(SwapTokenScreenTestTags.SWAP_BUTTON), ) { when (state.changeCardsButtonState) { ChangeCardsButtonState.UPDATE_IN_PROGRESS -> { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 81430c0cfa..8b8331fff7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -39,6 +40,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType @@ -181,7 +183,8 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie top = TangemTheme.dimens.spacing14, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - ), + ) + .testTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -206,7 +209,8 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier - .align(Alignment.CenterVertically), + .align(Alignment.CenterVertically) + .testTag(SwapTokenScreenTestTags.BALANCE), ) } } else { @@ -254,7 +258,7 @@ private fun Content( color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h2, fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), - modifier = sumTextModifier, + modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD), ) } else { RectangleShimmer( @@ -269,7 +273,7 @@ private fun Content( val focusRequester = remember { FocusRequester() } AutoSizeTextField( - modifier = sumTextModifier, + modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD), focusRequester = focusRequester, textFieldValue = textFieldValue ?: TextFieldValue(), onAmountChange = { type.onAmountChanged(it) }, @@ -344,7 +348,8 @@ private fun Content( modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing4) .width(TangemTheme.dimens.size40) - .height(TangemTheme.dimens.size12), + .height(TangemTheme.dimens.size12) + .testTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER), radius = TangemTheme.dimens.radius3, ) } @@ -365,7 +370,8 @@ fun Token( .padding( end = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing12, - ), + ) + .testTag(SwapTokenScreenTestTags.TOKEN), verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.End, ) { @@ -382,7 +388,9 @@ fun Token( maxLines = 1, style = TangemTheme.typography.subtitle2, textAlign = TextAlign.Center, - modifier = Modifier.defaultMinSize(minWidth = TangemTheme.dimens.size80), + modifier = Modifier + .defaultMinSize(minWidth = TangemTheme.dimens.size80) + .testTag(SwapTokenScreenTestTags.TOKEN_NAME), ) } } @@ -403,7 +411,8 @@ private fun TokenIcon( Box( modifier = Modifier .padding(end = TangemTheme.dimens.spacing16) - .size(TangemTheme.dimens.size42), + .size(TangemTheme.dimens.size42) + .testTag(SwapTokenScreenTestTags.TOKEN_ICON), ) { val tokenImageModifier = Modifier .align(Alignment.BottomStart) diff --git a/features/tangempay/details/api/.gitignore b/features/tangempay/details/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/api/build.gradle.kts b/features/tangempay/details/api/build.gradle.kts new file mode 100644 index 0000000000..77acdd5c22 --- /dev/null +++ b/features/tangempay/details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt new file mode 100644 index 0000000000..393e589bce --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tangempay + +interface TangemPayFeatureToggles { + val isTangemPayEnabled: Boolean +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt new file mode 100644 index 0000000000..64e11cecbe --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface TangemPayDetailsComponent : ComposableContentComponent { + @Suppress("EmptyDefaultConstructor") // Will add params in Next PRs + class Params() + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/.gitignore b/features/tangempay/details/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts new file mode 100644 index 0000000000..4f9fb1c110 --- /dev/null +++ b/features/tangempay/details/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt new file mode 100644 index 0000000000..a51c11a3bc --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTangemPayFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TangemPayFeatureToggles { + override val isTangemPayEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt new file mode 100644 index 0000000000..2c55cb6a3a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.decompose.context.AppComponentContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: TangemPayDetailsComponent.Params, +) : AppComponentContext by appComponentContext, TangemPayDetailsComponent { + + @Composable + override fun Content(modifier: Modifier) { + Box(modifier.fillMaxSize().background(Color.Red)) + // TODO("[REDACTED_JIRA]") + } + + @AssistedFactory + interface Factory : TangemPayDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: TangemPayDetailsComponent.Params, + ): DefaultTangemPayDetailsComponent + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt new file mode 100644 index 0000000000..4cd92fc806 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.components.DefaultTangemPayDetailsComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayDetailsFeatureModule { + + @Binds + @Singleton + fun bindTangemPayDetailsComponentFactory( + factory: DefaultTangemPayDetailsComponent.Factory, + ): TangemPayDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt new file mode 100644 index 0000000000..a6ea142d28 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.tangempay.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemPayDetailsModule { + + @Provides + @Singleton + fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { + return DefaultTangemPayFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/tangempay/main/api/.gitignore b/features/tangempay/main/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts new file mode 100644 index 0000000000..15fb515b8b --- /dev/null +++ b/features/tangempay/main/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/main/impl/.gitignore b/features/tangempay/main/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts new file mode 100644 index 0000000000..eb442c8f69 --- /dev/null +++ b/features/tangempay/main/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt new file mode 100644 index 0000000000..d431c7cd85 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt @@ -0,0 +1,13 @@ +package com.tangem.features.tester.api + +import android.view.KeyEvent +import androidx.lifecycle.DefaultLifecycleObserver + +/** + * Interface for observing to key events in a lifecycle-aware manner. + * Implementations should handle key events and return true if the event was consumed. + */ +interface KeyEventObserver : DefaultLifecycleObserver { + + fun dispatchKeyEvent(event: KeyEvent): Boolean +} \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt index a6338eaa73..b16f8b366d 100644 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt @@ -1,7 +1,5 @@ package com.tangem.features.tester.api -import androidx.lifecycle.DefaultLifecycleObserver - /** * Interface for launching the tester menu * @@ -9,6 +7,9 @@ import androidx.lifecycle.DefaultLifecycleObserver */ interface TesterMenuLauncher { - /** Observer for detecting shake events and launching the tester menu */ - val launchOnShakeObserver: DefaultLifecycleObserver + /** + * Observer for key events to open the tester menu. + * Implementations should handle key events and return true if the event was consumed. + */ + val launchOnKeyEventObserver: KeyEventObserver } \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index bd1dd6bf0f..6be7aca95c 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { /** Compose */ implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt index 859469177f..48e8ef8bfd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt @@ -6,15 +6,15 @@ import com.tangem.features.tester.api.TesterMenuLauncher import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext @Module -@InstallIn(SingletonComponent::class) +@InstallIn(ActivityComponent::class) internal object TesterMenuLauncherModule { @Provides - fun provideTesterMenuLauncher(@ApplicationContext context: Context): TesterMenuLauncher { - return DefaultTesterMenuLauncher(context = context) + fun provideTesterMenuLauncher(@ActivityContext context: Context): TesterMenuLauncher { + return DefaultTesterMenuLauncher(context) } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 3c28e87b46..8cd7fd94f1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -92,6 +93,7 @@ internal class TesterActivity : ComposeActivity() { innerTesterRouter.open(route) }, ), + modifier = Modifier.systemBarsPadding(), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt index 772c22afab..277ab7fb86 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt @@ -46,9 +46,9 @@ internal fun ExcludedBlockchainsScreen(state: ExcludedBlockchainsScreenUM, modif TangemTopAppBar( title = resourceReference(R.string.excluded_blockchains), startButton = TopAppBarButtonUM.Back(onBackClicked = state.popBack), - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_refresh_24, - onIconClicked = state.onRecoverClick, + onClicked = state.onRecoverClick, ), ) }, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index da2e67b08d..b369752bd1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -31,11 +31,11 @@ import kotlinx.collections.immutable.toImmutableList */ @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun TesterMenuScreen(state: TesterMenuUM) { +internal fun TesterMenuScreen(state: TesterMenuUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) LazyColumn( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), ) { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt index fbd8a860ff..1c1fc90ee1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt @@ -1,53 +1,15 @@ package com.tangem.feature.tester.presentation.navigation import android.content.Context -import android.content.Intent -import android.hardware.Sensor -import android.hardware.SensorManager -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.tangem.feature.tester.presentation.TesterActivity import com.tangem.features.tester.api.TesterMenuLauncher /** - * Default implementation of [TesterMenuLauncher] that listens for shake events using the device's accelerometer. - * When a shake is detected, it opens the tester menu. + * Default implementation of [TesterMenuLauncher] that listens for double-press events + * of the volume down button. When a double press is detected, it opens the tester menu. * - * @param context the application context used to access system services - * -[REDACTED_AUTHOR] + * @param context the application context used to launch the tester menu */ internal class DefaultTesterMenuLauncher(private val context: Context) : TesterMenuLauncher { - override val launchOnShakeObserver: DefaultLifecycleObserver by lazy(LazyThreadSafetyMode.NONE) { - createObserver(context) - } - - private fun createObserver(context: Context): DefaultLifecycleObserver { - return object : DefaultLifecycleObserver { - private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager - private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - private val shakeEventListener = ShakeEventListener(action = ::openTesterMenu) - - override fun onResume(owner: LifecycleOwner) { - accelerometer?.let { - sensorManager.registerListener( - /* listener = */ shakeEventListener, - /* sensor = */ it, - /* samplingPeriodUs = */ SensorManager.SENSOR_DELAY_NORMAL, - ) - } - } - - override fun onPause(owner: LifecycleOwner) { - sensorManager.unregisterListener(shakeEventListener) - } - } - } - - private fun openTesterMenu() { - val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - context.startActivity(intent) - } + override val launchOnKeyEventObserver = VolumeButtonDoublePressObserver(context) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt deleted file mode 100644 index 133628c9ac..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.tester.presentation.navigation - -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import kotlin.math.sqrt - -/** - * Listener for device shake events. - * - * This class implements [SensorEventListener] and is used to detect device shaking based on accelerometer data. - * When a shake is detected, the provided action is invoked. - * - * @property action lambda function to be called when a shake is detected - * -[REDACTED_AUTHOR] - */ -internal class ShakeEventListener(private val action: () -> Unit) : SensorEventListener { - - private var lastShakeTime = 0L - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit - - override fun onSensorChanged(event: SensorEvent?) { - if (event?.sensor?.type != Sensor.TYPE_ACCELEROMETER) return - - val acceleration = calculateAcceleration(event = event) - - val currentTime = System.currentTimeMillis() - val currentShakeInterval = currentTime - lastShakeTime - - if (acceleration > SHAKE_THRESHOLD && currentShakeInterval > SHAKE_INTERVAL_MS) { - lastShakeTime = currentTime - action() - } - } - - private fun calculateAcceleration(event: SensorEvent): Float { - val (x, y, z) = event.toXYZ() - - return sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH - } - - private fun SensorEvent.toXYZ() = Triple(values[0], values[1], values[2]) - - private companion object { - private const val SHAKE_THRESHOLD: Float = 12f - private const val SHAKE_INTERVAL_MS: Long = 1000 - } -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt new file mode 100644 index 0000000000..c683121653 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt @@ -0,0 +1,64 @@ + +package com.tangem.feature.tester.presentation.navigation + +import android.content.Context +import android.content.Intent +import android.os.SystemClock +import android.view.KeyEvent +import androidx.lifecycle.LifecycleOwner +import com.tangem.feature.tester.presentation.TesterActivity +import com.tangem.features.tester.api.KeyEventObserver + +/** + * A key event observer that listens for volume down button presses to open the tester menu. + * It requires two consecutive volume down presses within a specified interval to trigger the menu. + */ +internal class VolumeButtonDoublePressObserver(private val context: Context) : KeyEventObserver { + + private var lastVolumeDownTime = 0L + private var volumeDownCount = 0 + private var isReady = false + + override fun onResume(owner: LifecycleOwner) { + isReady = true + } + + override fun onPause(owner: LifecycleOwner) { + isReady = false + } + + /** + * Returns true if the tester menu was opened. + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (!isReady) return false + + if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + val now = SystemClock.elapsedRealtime() + volumeDownCount = if (now - lastVolumeDownTime <= DOUBLE_PRESS_INTERVAL_MS) { + volumeDownCount + 1 + } else { + 1 + } + lastVolumeDownTime = now + + if (volumeDownCount == REQUIRED_PRESS_COUNT) { + volumeDownCount = 0 + openTesterMenu(context) + return true + } + } + + return false + } + + private fun openTesterMenu(context: Context) { + val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + companion object { + private const val DOUBLE_PRESS_INTERVAL_MS = 300L + private const val REQUIRED_PRESS_COUNT = 2 + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 4e929dfdb3..4bcd8ce768 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.coil) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt index 9a17d9be3a..04b8498da4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt @@ -4,8 +4,8 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus internal class TokenDetailsCurrencyStatusAnalyticsSender( private val analyticsEventHandler: AnalyticsEventHandler, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 186939ab0a..f5352430b4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -33,23 +33,23 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.ShouldShowPromoTokenUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase -import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent @@ -67,9 +67,8 @@ import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener @@ -133,7 +132,6 @@ internal class TokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, - getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, private val appRouter: AppRouter, private val router: InnerTokenDetailsRouter, private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, @@ -169,7 +167,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index ac4af1dc72..95139be280 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -4,9 +4,9 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.utils.Provider diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 75fd755d3b..03fbaa9901 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -9,10 +9,10 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index a18b9be6e1..ec37185065 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -16,9 +16,9 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index ac4a41d6a3..3b3acb4d24 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -11,8 +11,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, - private val getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, ) : Converter { @@ -38,7 +37,7 @@ internal class TokenDetailsSkeletonStateConverter( override fun convert(value: CryptoCurrency): TokenDetailsState { val iconState = iconStateConverter.convert(value) - val isSupportedInMobileApp = getStakingIntegrationIdUseCase(value.id).isNullOrBlank().not() + val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = value.id) != null return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 82a04112f4..f6fb474788 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -8,13 +8,13 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.RewardBlockType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.RewardBlockType -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.staking.utils.getTotalStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 16726e8a98..22365b4892 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -15,21 +15,20 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.staking.GetStakingIntegrationIdUseCase +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -55,7 +54,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, symbol: String, decimals: Int, ) { @@ -64,7 +62,6 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, networkHasDerivationUseCase = networkHasDerivationUseCase, - getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index d418812ee0..27a03a1006 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -2,8 +2,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus -import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 5b3760c7f4..46a530ddbe 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -6,11 +6,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index c646572718..61a05030ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -3,18 +3,18 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampStatusUseCase import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.OnrampStatus.Status.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index ae100c11a3..d1110e97fa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -20,11 +20,11 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.toImmutableList @Suppress("DestructuringDeclarationWithTooManyEntries") @@ -124,7 +124,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -154,7 +154,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 350dad39ff..e4b8b6ad00 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState @@ -39,6 +41,7 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod text = state.name, style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.TOKEN_TITLE), ) NetworkInfoText(state.currency) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt index 8e123b32df..fc4a214ee0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt @@ -107,5 +107,6 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider, val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, + val isWalletBackedUp: Boolean = true, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 1906bde484..f9d0b633b2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -15,10 +15,16 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO @@ -31,8 +37,6 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.* import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.analytics.WalletSettingsAnalyticEvents @@ -43,7 +47,6 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.ItemsBuilder -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList @@ -75,7 +78,6 @@ internal class WalletSettingsModel @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val settingsManager: SettingsManager, private val permissionsRepository: PermissionRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val notificationsRepository: NotificationsRepository, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, ) : Model() { @@ -90,9 +92,29 @@ internal class WalletSettingsModel @Inject constructor( items = persistentListOf(), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, + isWalletBackedUp = true, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + closeBs() + } + } + } + init { combine( getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), @@ -101,6 +123,10 @@ internal class WalletSettingsModel @Inject constructor( ) { maybeWallet, nftEnabled, notificationsEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + val isWalletBackedUp = when (wallet) { + is UserWallet.Hot -> wallet.backedUp + is UserWallet.Cold -> true + } val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && !getIsHuaweiDeviceWithoutGoogleServicesUseCase() state.update { value -> @@ -113,8 +139,8 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), - isHotWalletEnabled = hotWalletFeatureToggles.isHotWalletEnabled, ), + isWalletBackedUp = isWalletBackedUp, ) } } @@ -139,46 +165,58 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, isNotificationsPermissionGranted: Boolean, - isHotWalletEnabled: Boolean, - ): PersistentList = itemsBuilder.buildItems( - userWalletId = userWallet.walletId, - userWalletName = userWallet.name, - isReferralAvailable = userWallet !is UserWallet.Cold || userWallet.cardTypesResolver.isTangemWallet(), - isLinkMoreCardsAvailable = userWallet is UserWallet.Cold && - userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, - isManageTokensAvailable = userWallet.isMultiCurrency, - isRenameWalletAvailable = isRenameWalletAvailable, - renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, - isNFTFeatureEnabled = userWallet.isMultiCurrency, - isNFTEnabled = isNFTEnabled, - onCheckedNFTChange = ::onCheckedNFTChange, - forgetWallet = { - val message = DialogMessage( - message = resourceReference(R.string.user_wallet_list_delete_prompt), - firstActionBuilder = { - EventMessageAction( - title = resourceReference(R.string.common_delete), - warning = true, - onClick = ::forgetWallet, - ) - }, - secondActionBuilder = { cancelAction() }, - ) + ): PersistentList { + val isMultiCurrency = when (userWallet) { + is UserWallet.Cold -> userWallet.isMultiCurrency + is UserWallet.Hot -> true + } + return itemsBuilder.buildItems( + userWallet = userWallet, + userWalletName = userWallet.name, + isReferralAvailable = when (userWallet) { + is UserWallet.Cold -> userWallet.cardTypesResolver.isTangemWallet() + is UserWallet.Hot -> false + }, + isLinkMoreCardsAvailable = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup + is UserWallet.Hot -> false + }, + isManageTokensAvailable = isMultiCurrency, + isRenameWalletAvailable = isRenameWalletAvailable, + renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, + isNFTFeatureEnabled = isMultiCurrency, + isNFTEnabled = isNFTEnabled, + onCheckedNFTChange = ::onCheckedNFTChange, + forgetWallet = { + val message = DialogMessage( + message = resourceReference(R.string.user_wallet_list_delete_prompt), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_delete), + warning = true, + onClick = ::forgetWallet, + ) + }, + secondActionBuilder = { cancelAction() }, + ) - messageSender.send(message) - }, - onLinkMoreCardsClick = { - userWallet.requireColdWallet() - onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) - }, - onReferralClick = { onReferralClick(userWallet) }, - isNotificationsEnabled = isNotificationsEnabled, - isNotificationsFeatureEnabled = isNotificationsFeatureEnabled, - isNotificationsPermissionGranted = isNotificationsPermissionGranted, - onCheckedNotificationsChanged = ::onCheckedNotificationsChange, - onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, - isHotWalletEnabled = isHotWalletEnabled, - ) + messageSender.send(message) + }, + onLinkMoreCardsClick = { + when (userWallet) { + is UserWallet.Cold -> onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) + is UserWallet.Hot -> Unit + } + }, + onReferralClick = { onReferralClick(userWallet) }, + isNotificationsEnabled = isNotificationsEnabled, + isNotificationsFeatureEnabled = isNotificationsFeatureEnabled, + isNotificationsPermissionGranted = isNotificationsPermissionGranted, + onCheckedNotificationsChanged = ::onCheckedNotificationsChange, + onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, + onAccessCodeClick = ::onAccessCodeClick, + ) + } private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { val config = DialogConfig.RenameWallet( @@ -203,7 +241,7 @@ internal class WalletSettingsModel @Inject constructor( if (hasUserWallets) { router.pop() } else { - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Home()) } } @@ -321,4 +359,12 @@ internal class WalletSettingsModel @Inject constructor( router.push(AppRoute.ReferralProgram(userWallet.walletId)) } } + + private fun onAccessCodeClick() { + if (!state.value.isWalletBackedUp) { + messageSender.send(makeBackupAtFirstAlertBS) + } else { + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + } + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 1349f34906..5086f22579 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -6,12 +6,15 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.hot.sdk.model.HotWalletId import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -25,7 +28,7 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") fun buildItems( - userWalletId: UserWalletId, + userWallet: UserWallet, userWalletName: String, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, @@ -43,25 +46,18 @@ internal class ItemsBuilder @Inject constructor( renameWallet: () -> Unit, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, - isHotWalletEnabled: Boolean, + onAccessCodeClick: () -> Unit, ): PersistentList = persistentListOf() .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) - .run { - if (isNFTFeatureEnabled) { - add(buildNFTItem(isNFTEnabled, onCheckedNFTChange)) - } else { - this - } - } + .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .add( buildCardItem( - userWalletId = userWalletId, + userWallet = userWallet, isLinkMoreCardsAvailable = isLinkMoreCardsAvailable, isReferralAvailable = isReferralAvailable, isManageTokensAvailable = isManageTokensAvailable, onLinkMoreCardsClick = onLinkMoreCardsClick, onReferralClick = onReferralClick, - isHotWalletEnabled = isHotWalletEnabled, ), ) .addAll( @@ -73,8 +69,27 @@ internal class ItemsBuilder @Inject constructor( onNotificationsDescriptionClick = onNotificationsDescriptionClick, ), ) + .addAll( + buildNFTItems( + isNFTFeatureEnabled = isNFTFeatureEnabled, + isNFTEnabled = isNFTEnabled, + onCheckedNFTChange = onCheckedNFTChange, + ), + ) .add(buildForgetItem(forgetWallet)) + private fun buildNFTItems( + isNFTFeatureEnabled: Boolean, + isNFTEnabled: Boolean, + onCheckedNFTChange: (Boolean) -> Unit, + ): List { + return if (isNFTFeatureEnabled) { + listOf(buildNFTItem(isNFTEnabled, onCheckedNFTChange)) + } else { + emptyList() + } + } + private fun buildNotificationItems( isNotificationsFeatureEnabled: Boolean, isNotificationsPermissionGranted: Boolean, @@ -133,17 +148,35 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") private fun buildCardItem( - userWalletId: UserWalletId, + userWallet: UserWallet, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, - isHotWalletEnabled: Boolean, ) = WalletSettingsItemUM.WithItems( id = "card", description = resourceReference(R.string.settings_card_settings_footer), blocks = buildList { + val userWalletId = userWallet.walletId + val isHotWallet = userWallet is UserWallet.Hot + if (isHotWallet) { + val hasBackup = userWallet.backedUp + BlockUM( + text = resourceReference(R.string.common_backup), + iconRes = R.drawable.ic_more_cards_24, + onClick = { router.push(AppRoute.WalletBackup(userWalletId)) }, + label = if (hasBackup) { + null + } else { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + }, + ).let(::add) + } + if (isManageTokensAvailable) { BlockUM( text = resourceReference(R.string.add_tokens_title), @@ -163,17 +196,11 @@ internal class ItemsBuilder @Inject constructor( ).let(::add) } - BlockUM( - text = resourceReference(R.string.card_settings_title), - iconRes = R.drawable.ic_card_settings_24, - onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, - ).let(::add) - - if (isHotWalletEnabled) { + if (!isHotWallet) { BlockUM( - text = resourceReference(R.string.common_backup), - iconRes = R.drawable.ic_more_cards_24, - onClick = { router.push(AppRoute.WalletBackup(userWalletId)) }, + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, ).let(::add) } @@ -199,4 +226,36 @@ internal class ItemsBuilder @Inject constructor( ), ), ) + + private fun buildAccessCodeItem(userWallet: UserWallet, onItemClick: () -> Unit): List { + return when (userWallet) { + is UserWallet.Cold -> emptyList() + is UserWallet.Hot -> buildHotWalletAccessCodeItem(userWallet, onItemClick) + } + } + + private fun buildHotWalletAccessCodeItem( + userWallet: UserWallet.Hot, + onItemClick: () -> Unit, + ): List { + val isCodeSet = userWallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + return listOf( + WalletSettingsItemUM.WithItems( + id = "access_code", + description = resourceReference(R.string.wallet_settings_access_code_description), + blocks = persistentListOf( + BlockUM( + text = if (isCodeSet) { + resourceReference(R.string.wallet_settings_change_access_code_title) + } else { + resourceReference(R.string.wallet_settings_set_access_code_title) + }, + iconRes = R.drawable.ic_lock_24, + onClick = onItemClick, + accentType = BlockUM.AccentType.ACCENT, + ), + ), + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt index 3f30b7eebe..674541796f 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt @@ -14,6 +14,7 @@ interface UserWalletsFetcher { fun create( messageSender: UiMessageSender, onlyMultiCurrency: Boolean, + authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): UserWalletsFetcher } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 77dd37708d..5d506674c2 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.paging) implementation(deps.compose.reorderable) @@ -114,6 +113,7 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.kyc.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index 7aeebcd81e..4f7dfafd03 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -12,12 +12,12 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase import com.tangem.domain.tokens.ToggleTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensIntents import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt index 2ecee965b3..466f15890e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt @@ -102,7 +102,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( } else { tokenListStore.clear() stateHolder.clear() - appRouter.replaceAll(AppRoute.Home) + appRouter.replaceAll(AppRoute.Home()) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 06cea48302..cc5a440140 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -4,17 +4,17 @@ import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.TokensAction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index b58f51c8ef..29c08244a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -30,8 +30,11 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -41,15 +44,12 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.impl.R diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index e88a4566e0..5e3757b744 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -9,25 +9,26 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase @@ -113,6 +114,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { @@ -241,8 +243,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { neverToSuggestRateAppUseCase() - val scanResponse = - getSelectedUserWallet()?.requireColdWallet()?.scanResponse ?: return@launch // TODO [REDACTED_TASK_KEY] + val userWallet = getSelectedUserWallet() ?: return@launch + + if (userWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + + val scanResponse = userWallet.requireColdWallet().scanResponse val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch sendFeedbackEmailUseCase(type = FeedbackEmailType.RateCanBeBetter(cardInfo = cardInfo)) @@ -283,7 +290,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onSupportClick() { - val scanResponse = getSelectedUserWallet()?.requireColdWallet()?.scanResponse ?: return // TODO [REDACTED_TASK_KEY] + val userWallet = getSelectedUserWallet() ?: return + + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + + val scanResponse = userWallet.requireColdWallet().scanResponse val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return modelScope.launch { @@ -374,7 +387,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onFinishWalletActivationClick() { - // TODO implement wallet activation process + val userWallet = getSelectedUserWallet() ?: return + appRouter.push(AppRoute.WalletActivation(userWallet.walletId)) } private suspend fun fetchCryptoCurrencies(userWalletId: UserWalletId, currencies: List) { @@ -399,11 +413,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( .onLeft { Timber.e("Unable to fetch quotes: $it") } }, async { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associate { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) .onLeft { Timber.e("Unable to fetch yield balances: $it") } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 8b112a4dd6..193ed15375 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt index fc72339e53..74bec371ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt index 4ff807e1f8..8600d8beaf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.domain.models.TokensSortType -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList internal fun TokenList.disableSortingByBalance(): TokenList { return when (this) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt index 12a713c0ff..d561e3b262 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter import com.tangem.domain.models.TokensSortType -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 87c169926c..21f42375ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -7,9 +7,9 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt index 0205df476a..73ecfe3f79 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt index 455e896da9..770d63c0f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2567421144..8c39cfc9e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -6,12 +6,12 @@ import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.redux.StateDialog -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import kotlinx.coroutines.channels.BufferOverflow @@ -75,7 +75,7 @@ internal class DefaultWalletRouter @Inject constructor( } override fun openStoriesScreen() { - router.push(AppRoute.Home) + router.push(AppRoute.Home()) } override fun isWalletLastScreen(): Boolean { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index c33e3a6652..b1670c0110 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,8 +2,8 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.navigation.WalletRoute diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 1ce39be795..bf5b27d5ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -13,11 +13,11 @@ import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 2e1f9c25f6..248f54a3e6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -8,13 +8,13 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase @@ -266,8 +266,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) { if (userWallet !is UserWallet.Hot) return - // TODO [REDACTED_TASK_KEY] set an actual value - val shouldShowFinishActivation = false + val shouldShowFinishActivation = !userWallet.backedUp addIf( element = WalletNotification.FinishWalletActivation( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index ab95132fed..64d80d9754 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -6,15 +6,15 @@ import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt index 167deff237..1aa129675c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.SharingStarted diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index 6c985e4728..2af73a2491 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.domain.onramp.GetOnrampStatusUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt index 3e917b7417..58da9fca4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import kotlinx.coroutines.flow.* import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt index 1657e84545..3f365ebc8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -2,29 +2,44 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import timber.log.Timber class WalletNameMigrationUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, private val walletNamesMigrationRepository: WalletNamesMigrationRepository, ) { suspend operator fun invoke() { - val wallets = userWalletsListManager.userWalletsSync - if (walletNamesMigrationRepository.isMigrationDone()) { return } - val existingNames: MutableSet = mutableSetOf() - wallets.indices.forEach { i -> - val defaultName = wallets[i].name - val suggestedWalletName = suggestedWalletName(defaultName, existingNames) - if (defaultName != suggestedWalletName) { - userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + if (useNewListRepository) { + val wallets = userWalletsListRepository.userWalletsSync() + val existingNames: MutableSet = mutableSetOf() + wallets.forEach { + val defaultName = it.name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) + } + Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) + } + } else { + val wallets = userWalletsListManager.userWalletsSync + val existingNames: MutableSet = mutableSetOf() + wallets.indices.forEach { i -> + val defaultName = wallets[i].name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + } + Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } - Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt index 586234c6d6..6561164dd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -1,9 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.extensions.isZero +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import javax.inject.Inject internal class WalletWithFundsChecker @Inject constructor( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index 822e377075..b3ebdd5713 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -1,16 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import kotlinx.collections.immutable.toPersistentList import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index 4c6f769a5e..4e5b37f5d9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 54ff2afed0..e8b3b2446f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import timber.log.Timber internal class SetTokenListTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 1201f8807a..2accc6f5b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -9,16 +9,12 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.common.util.getCardsCount -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.exception.RefreshTokenExpiredException import com.tangem.domain.visa.model.VisaCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.utils.extensions.isZero import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 5099b4e6b9..e717d41de5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState @@ -40,8 +39,8 @@ internal class UpdateWalletCardsCountTransformer( return when (this) { is WalletCardState.Content -> copy( additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), - imageResId = walletImageResolver.resolve(userWallet = userWallet.requireColdWallet()), // TODO [REDACTED_TASK_KEY] - cardCount = userWallet.requireColdWallet().getCardsCount(), // TODO [REDACTED_TASK_KEY] + imageResId = walletImageResolver.resolve(userWallet = userWallet), + cardCount = (userWallet as? UserWallet.Cold)?.getCardsCount(), ) else -> this } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 98c83885fa..97a13ed361 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -3,13 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig -import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 4df4316649..fe8a06fbc8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 3611aa8119..a2dc7210ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -4,11 +4,13 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( @@ -47,17 +49,18 @@ internal class SingleWalletMarketPriceConverter( } private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { - val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatRate = status.fiatRate ?: return DASH_SIGN - return BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = fiatRate, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return fiatRate.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).uncapped() + } } private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { - val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val priceChange = status.priceChange ?: return DASH_SIGN return priceChange.format { percent() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index 5b487cb700..73813e73d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -15,9 +15,9 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 9a3c8407b4..ec332f336e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -7,10 +7,10 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index 99b787897e..f1ea722bd8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -2,13 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxDetails -import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList import org.joda.time.DateTimeZone @@ -71,11 +71,12 @@ internal class VisaTxDetailsBottomSheetConverter( } private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount, - fiatCurrencyCode = fiatCurrency.currencyCode, - fiatCurrencySymbol = fiatCurrency.symbol, - ) + return amount.format { + fiat( + fiatCurrencyCode = fiatCurrency.currencyCode, + fiatCurrencySymbol = fiatCurrency.symbol, + ) + } } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 62d00e4c3c..0ec063a2f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -4,13 +4,13 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxHistoryItem -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.impl.R import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -29,11 +29,12 @@ internal class VisaTxHistoryItemStateConverter( txHash = value.id, amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }, // Show tx fiat amount instead of tx time - time = BigDecimalFormatter.formatFiatAmount( - fiatAmount = value.fiatAmount, - fiatCurrencyCode = value.fiatCurrency.currencyCode, - fiatCurrencySymbol = value.fiatCurrency.symbol, - ), + time = value.fiatAmount.format { + fiat( + fiatCurrencyCode = value.fiatCurrency.currencyCode, + fiatCurrencySymbol = value.fiatCurrency.symbol, + ) + }, status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.INCOMING, iconRes = R.drawable.ic_arrow_up_24, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 9307b4c066..a475345feb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -45,8 +45,8 @@ internal class WalletLoadingStateFactory( walletCardState = WalletCardState.Loading( id = userWallet.walletId, title = userWallet.name, - additionalInfo = null, // TODO [REDACTED_TASK_KEY] - imageResId = null, // TODO [REDACTED_TASK_KEY] + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + imageResId = null, dropDownItems = persistentListOf(), ), buttons = createMultiWalletActions(userWallet), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index e98689e34e..08c415f5b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -6,10 +6,10 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index e9dbc7eb6f..b719923f57 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -6,11 +6,11 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index 1fa58e9e7d..4532a34ec2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -7,11 +7,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index 7600075e6a..a2d9cbdd89 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -5,13 +5,13 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index e93d8962f6..80027a5ec1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index df61988921..e5deefbf1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -5,14 +5,14 @@ import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt index e82c67b4fe..8465d3fca3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp @@ -14,6 +15,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.ActionButtonContent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.MainScreenTestTags /** [REDACTED_AUTHOR] @@ -48,7 +50,7 @@ internal fun MultiCurrencyAction( paddingBetweenIconAndText = 4.dp, ) }, - modifier = modifier, + modifier = modifier.testTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON), color = TangemTheme.colors.button.secondary, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index ce71cf24a6..7a3b9accba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -43,7 +43,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @Assisted private val onWalletClick: (UserWalletId) -> Unit, @Assisted private val messageSender: UiMessageSender, - @Assisted private val onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, + @Assisted("authMode") private val authMode: Boolean, private val getCardImageUseCase: GetCardImageUseCase, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { @@ -54,7 +55,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override val userWallets: Flow> = walletsFlow.transformLatest { wallets -> - val uiModels = UserWalletItemUMConverter(onClick = onWalletClick).convertList(wallets) + val uiModels = UserWalletItemUMConverter( + onClick = onWalletClick, + authMode = authMode, + ).convertList(wallets) .toImmutableList() emit(uiModels) @@ -132,6 +136,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, artwork = artworks[userWallet.walletId], + authMode = authMode, ) .convert(userWallet) } @@ -149,7 +154,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( interface Factory : UserWalletsFetcher.Factory { override fun create( messageSender: UiMessageSender, - onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean, + @Assisted("authMode") authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): DefaultUserWalletsFetcher } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 38f270b631..666eb2eb8f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -133,9 +133,8 @@ internal class WcConnectionsModel @Inject constructor( private fun getInitialState(): WcConnectionsState { return WcConnectionsState( topAppBarConfig = WcConnectionsTopAppBarConfig( - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = router::pop, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = router::pop, enabled = true, ), disconnectAllItem = TangemDropdownMenuItem( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt index 452502ee96..25acf5033f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.TopAppBarButton import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.components.snackbar.TangemSnackbarHost @@ -255,13 +256,10 @@ private fun ConnectionsTopBar( actionIconContentColor = TangemTheme.colors.icon.primary1, ), navigationIcon = { - IconButton(onClick = config.startButtonUM.onIconClicked) { - Icon( - painter = painterResource(id = config.startButtonUM.iconRes), - tint = TangemTheme.colors.icon.primary1, - contentDescription = "Back", - ) - } + TopAppBarButton( + button = config.startButtonUM, + tint = TangemTheme.colors.icon.primary1, + ) }, title = { Text( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt index b478dee7cd..a527826b41 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -118,9 +118,8 @@ internal object WcConnectionsPreviewData { ) val stateWithEmptyConnections = WcConnectionsState( topAppBarConfig = WcConnectionsTopAppBarConfig( - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = {}, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = {}, enabled = true, ), disconnectAllItem = TangemDropdownMenuItem( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index bcfd5129b1..727e85c485 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -29,6 +29,7 @@ internal class WcUserWalletsFetcher( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = true, + authMode = false, onWalletClick = { onWalletSelected(it) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt index d2a25d9cd4..292804e584 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.components.common import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 @@ -50,6 +51,7 @@ internal fun getWcCommonScreen( callback = model, feeStateConfiguration = model.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, + analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME, ), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 62f838c9b7..a974a6874f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams @@ -30,6 +31,7 @@ internal class WcSendTransactionComponent( feeCryptoCurrencyStatus = model.cryptoCurrencyStatus, feeStateConfiguration = model.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, + analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME, ), onResult = model::updateFee, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index 49aadb776f..c8f65f4c23 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -1,6 +1,6 @@ package com.tangem.features.walletconnect.transaction.converter -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 335160da29..d3f10b517e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -21,11 +21,11 @@ import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.core.lce.Lce +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError.UserCancelledError import com.tangem.domain.transaction.usecase.GetFeeUseCase diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt index 35e9a28c76..b8c0eb3057 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt @@ -30,7 +30,7 @@ internal fun WcAddressItem(address: String, modifier: Modifier = Modifier) { ) Text( modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - text = stringResourceSafe(R.string.wc_common_address), + text = stringResourceSafe(R.string.common_address), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, maxLines = 1, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt index c628689c04..09b3659efd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils import com.tangem.features.walletconnect.impl.R diff --git a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt index 50b914548b..bc7092a29c 100644 --- a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt +++ b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt @@ -1,14 +1,9 @@ package com.tangem.features.welcome -import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { - data class Params( - val intent: SerializableIntent?, - ) - - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 499c0ab82e..c175c3498a 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -13,11 +13,13 @@ android { dependencies { implementation(projects.features.welcome.api) + implementation(projects.features.wallet.api) /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.analytics) implementation(projects.common.routing) implementation(projects.common.ui) @@ -30,6 +32,8 @@ dependencies { /** Domain */ implementation(projects.domain.appCurrency) implementation(projects.domain.wallets) + implementation(projects.domain.card) + implementation(projects.domain.settings) /** DI */ implementation(deps.hilt.android) @@ -54,4 +58,5 @@ dependencies { implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt index 7bc6a1d982..8638078499 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt @@ -15,10 +15,10 @@ import dagger.assisted.AssistedInject internal class DefaultWelcomeComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: WelcomeComponent.Params, + @Assisted val params: Unit, ) : WelcomeComponent, AppComponentContext by context { - private val model: WelcomeModel = getOrCreateModel(params) + private val model: WelcomeModel = getOrCreateModel() @Composable override fun Content(modifier: Modifier) { @@ -32,6 +32,6 @@ internal class DefaultWelcomeComponent @AssistedInject constructor( @AssistedFactory interface Factory : WelcomeComponent.Factory { - override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent + override fun create(context: AppComponentContext, params: Unit): DefaultWelcomeComponent } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index da1f8c9c21..d3c8cab07a 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -1,18 +1,245 @@ package com.tangem.features.welcome.impl.model +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.features.welcome.impl.R +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.* import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val uiMessageSender: UiMessageSender, + private val userWalletsListRepository: UserWalletsListRepository, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val walletsRepository: WalletsRepository, + userWalletsFetcherFactory: UserWalletsFetcher.Factory, ) : Model() { val uiState: StateFlow - field = MutableStateFlow(WelcomeUM.Plain) + field = MutableStateFlow(WelcomeUM.Plain) + + private val walletsFetcher = userWalletsFetcherFactory.create( + messageSender = uiMessageSender, + onlyMultiCurrency = false, + authMode = true, + onWalletClick = { walletId -> + modelScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first { it.walletId == walletId } + onUserWalletClick(userWallet) + } + }, + ) + private val walletsFetcherJobHolder = JobHolder() + private val wallets = MutableStateFlow>(persistentListOf()) + private var routedOut = false + + init { + modelScope.launch { + userWalletsListRepository.load() + wallets.value = walletsFetcher.userWallets.first() + + launch { + walletsFetcher.userWallets + .collectLatest { + if (it.isEmpty()) { + router.replaceAll(AppRoute.Home()) + } + + wallets.value = it + } + } + + tryToUnlockRightAway() + } + } + + private fun tryToUnlockRightAway() { + modelScope.launch { + if (canUnlockWithBiometrics()) { + userWalletsListRepository.unlockAllWallets() + .onRight { + routedOut = true + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }) + setSelectWalletState() + } + } else { + tryToUnlockWithAccessCodeRightAway() + setSelectWalletState() + } + } + } + + private suspend fun tryToUnlockWithAccessCodeRightAway() { + if (onlyOneHotWalletWithAccessCode()) { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first() + uiState.value = WelcomeUM.Empty + unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode) + } + } + + private fun setSelectWalletState() { + modelScope.launch { + if (routedOut || uiState.value is WelcomeUM.SelectWallet) return@launch + + uiState.value = WelcomeUM.SelectWallet( + wallets = walletsFetcher.userWallets.first(), + showUnlockWithBiometricButton = canUnlockWithBiometrics(), + addWalletClick = ::addWalletClick, + onUnlockWithBiometricClick = { + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onRight { + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { /* ignore */ }) + } + } + }, + ) + + wallets.collectLatest { wallets -> + updateSelectState { + it.copy(wallets = wallets) + } + } + }.saveIn(walletsFetcherJobHolder) + } + + private fun addWalletClick() { + updateSelectState { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + content = AddWalletBottomSheetContentUM( + onOptionClick = ::onAddWalletOptionClick, + ), + onDismissRequest = { + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + }, + ), + ) + } + } + + private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) { + when (option) { + Create -> router.push(AppRoute.CreateWalletSelection) + Add -> router.push(AppRoute.AddExistingWallet) + Buy -> Unit // TODO + } + } + + private suspend fun onlyOneHotWalletWithAccessCode(): Boolean { + val userWalletsWithLock = userWalletsListRepository.userWalletsSync().filter { it.isLocked } + if (userWalletsWithLock.size != 1) return false + val wallet = userWalletsWithLock.first() + return wallet is UserWallet.Hot && wallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + } + + private fun onUserWalletClick(userWallet: UserWallet) = modelScope.launch { + if (userWallet.isLocked.not()) { + // If the wallet is not locked, we can proceed to the wallet screen directly + userWalletsListRepository.select(userWallet.walletId) + router.replaceAll(AppRoute.Wallet) + return@launch + } + + val unlockMethod = when (userWallet) { + is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan + is UserWallet.Hot -> { + uiState.value = WelcomeUM.Empty + UserWalletsListRepository.UnlockMethod.AccessCode + } + } + + unlockWallet(userWallet.walletId, unlockMethod) + setSelectWalletState() + } + + private suspend fun canUnlockWithBiometrics(): Boolean { + return canUseBiometryUseCase() && walletsRepository.useBiometricAuthentication() + } + + suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { + userWalletsListRepository.unlock(userWalletId, unlockMethod) + .onRight { + routedOut = true + userWalletsListRepository.select(userWalletId) + router.replaceAll(AppRoute.Wallet) + } + .onLeft { error -> + error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + } + } + + suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { + when (this) { + UnlockWalletError.AlreadyUnlocked -> { + // this should not happen, as we check for locked state before this + specificWalletId?.let { userWalletsListRepository.select(it) } + router.replaceAll(AppRoute.Wallet) + } + UnlockWalletError.ScannedCardWalletNotMatched -> { + // TODO Scanned card does not match the wallet + } + UnlockWalletError.UnableToUnlock -> { + // TODO Unable to unlock the wallet" + } + UnlockWalletError.UserCancelled -> onUserCancelled() + UnlockWalletError.UserWalletNotFound -> { + // This should never happen in this flow, as we always check for the wallet existence before unlocking + Timber.e("User wallet not found for unlock: $specificWalletId") + uiMessageSender.send( + SnackbarMessage(TextReference.Res(R.string.generic_error)), + ) + } + } + } + + private fun updateSelectState(block: (WelcomeUM.SelectWallet) -> WelcomeUM.SelectWallet) { + uiState.update { currentState -> + if (currentState is WelcomeUM.SelectWallet) { + block(currentState) + } else { + currentState + } + } + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt index 4331461929..5d26549787 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt @@ -12,16 +12,17 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM @Composable fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, - titleText = TextReference.Str("Add Wallet"), + titleText = resourceReference(R.string.auth_info_add_wallet_title), containerColor = TangemTheme.colors.background.tertiary, content = { Content(it) }, ) @@ -38,7 +39,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { ), ) { InputRowDefault( - text = TextReference.Str("Create New Wallet"), + text = resourceReference(R.string.home_button_create_new_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 0, @@ -49,7 +50,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, ) InputRowDefault( - text = TextReference.Str("Add Existing Wallet"), + text = resourceReference(R.string.home_button_add_existing_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 1, @@ -60,7 +61,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, ) InputRowDefault( - text = TextReference.Str("Buy Tangem Wallet"), + text = resourceReference(R.string.details_buy_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 2, diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt index caf6ecd9bd..04b1e81910 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -10,10 +10,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.extensions.TextReference +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.ui.state.WalletUM +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.persistentListOf @@ -34,10 +35,7 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { state = st, modifier = modifier, ) - is WelcomeUM.EnterAccessCode -> WelcomeEnterAccessCode( - state = st, - modifier = modifier, - ) + WelcomeUM.Empty -> {} } } } @@ -49,22 +47,30 @@ private fun Preview() { TangemThemePreview { val state = WelcomeUM.SelectWallet( wallets = persistentListOf( - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("3 cards"), - imageState = WalletUM.ImageState.Loading, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Loading, + balance = UserWalletItemUM.Balance.Loaded( + value = "1.2345 BTC", + isFlickering = false, + ), + isEnabled = true, onClick = {}, ), - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("Mobile wallet"), - imageState = WalletUM.ImageState.MobileWallet, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Failed, + imageState = UserWalletItemUM.ImageState.MobileWallet, + balance = UserWalletItemUM.Balance.Locked, + isEnabled = true, onClick = {}, ), ), ) - var currentState by remember { mutableStateOf(WelcomeUM.EnterAccessCode()) } + var currentState by remember { mutableStateOf(WelcomeUM.SelectWallet()) } Box { Welcome(currentState) @@ -74,11 +80,8 @@ private fun Preview() { onClick = { currentState = when (currentState) { is WelcomeUM.Plain -> state - is WelcomeUM.SelectWallet -> WelcomeUM.EnterAccessCode( - value = "", - onValueChange = {}, - ) - is WelcomeUM.EnterAccessCode -> WelcomeUM.Plain + is WelcomeUM.SelectWallet -> WelcomeUM.Empty + WelcomeUM.Empty -> WelcomeUM.Plain } }, ) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt deleted file mode 100644 index 3cd6085b58..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.appbar.TopAppBarButton -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.welcome.impl.ui.state.WelcomeUM - -@Suppress("MagicNumber") -@Composable -internal fun AnimatedContentScope.WelcomeEnterAccessCode( - state: WelcomeUM.EnterAccessCode, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxSize() - .statusBarsPadding(), - ) { - Column { - TopAppBarButton( - modifier = Modifier - .padding(12.dp), - button = TopAppBarButtonUM.Back(onBackClicked = state.onBackClick), - tint = TangemTheme.colors.icon.primary1, - ) - - SpacerH(68.dp) - - Text( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .align(Alignment.CenterHorizontally), - text = "Enter Access Code", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - - SpacerH24() - - Box( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - PinTextField( - length = 6, - isPasswordVisual = true, - value = state.value, - onValueChange = state.onValueChange, - ) - } - } - - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .imePadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Log in with biometric", - onClick = state.onUnlockWithBiometricClick, - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt index 13aaae14db..5b80e192ee 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt @@ -9,8 +9,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.welcome.impl.R @Composable @@ -26,4 +28,14 @@ internal fun WelcomePlain(modifier: Modifier = Modifier) { contentDescription = null, ) } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + WelcomePlain( + modifier = Modifier.fillMaxSize(), + ) + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index ed41fe5ad4..9b3bba406d 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -5,12 +5,9 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -20,14 +17,14 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp -import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.WalletUM import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -45,7 +42,7 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal TitleText() SpacerH12() - var actualWallets by remember { mutableStateOf>(persistentListOf()) } + var actualWallets by remember { mutableStateOf>(persistentListOf()) } Box(modifier = Modifier.weight(1f)) { LazyColumn( @@ -62,25 +59,33 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal verticalArrangement = Arrangement.spacedBy(8.dp), ) { itemsIndexed(actualWallets) { index, walletState -> - WalletItem( + UserWalletItem( + modifier = Modifier.fillMaxWidth(), state = walletState, - modifier = Modifier, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.field.primary, + ), ) } } BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Unlock all with biometric", - onClick = state.onUnlockWithBiometricClick, - ) + if (state.showUnlockWithBiometricButton) { + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .navigationBarsPadding() + .animateEnterExit(fadeIn(), fadeOut()), + text = stringResourceSafe( + R.string.user_wallet_list_unlock_all_with, + stringResourceSafe(id = R.string.common_biometrics), + ), + onClick = state.onUnlockWithBiometricClick, + ) + } } LaunchedEffect(state.wallets) { @@ -121,7 +126,7 @@ private fun AnimatedContentScope.TopBar(state: WelcomeUM.SelectWallet, modifier: TextButton( modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge), - text = "Add Wallet", + text = stringResourceSafe(R.string.auth_info_add_wallet_title), colors = TangemButtonsDefaults.defaultTextButtonColors.copy( contentColor = TangemTheme.colors.text.primary1, ), @@ -145,7 +150,7 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Welcome back!", + text = stringResourceSafe(R.string.auth_info_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -160,71 +165,9 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Select a wallet to log in", + text = stringResourceSafe(R.string.auth_info_subtitle), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, ) } -} - -@Suppress("MagicNumber") -@Composable -private fun WalletItem(state: WalletUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.secondary, TangemTheme.shapes.roundedCornersXMedium) - .clickable(onClick = state.onClick) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - WalletImage(state.imageState) - - SpacerW12() - - Column(Modifier.weight(1f)) { - Text( - text = state.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - - Text( - text = state.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun WalletImage(state: WalletUM.ImageState, modifier: Modifier = Modifier) { - when (state) { - WalletUM.ImageState.MobileWallet -> { - Box( - modifier = modifier - .size(36.dp) - .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_wallet_filled_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - } - else -> { - CardImage( - imageState = when (state) { - is WalletUM.ImageState.Image -> UserWalletItemUM.ImageState.Image(state.artwork) - WalletUM.ImageState.Loading -> UserWalletItemUM.ImageState.Loading - else -> error("") - }, - modifier = modifier, - ) - } - } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt deleted file mode 100644 index 216a28f7d2..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.welcome.impl.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.TextReference -import javax.annotation.concurrent.Immutable - -internal data class WalletUM( - val name: TextReference, - val subtitle: TextReference, - val imageState: ImageState, - val onClick: () -> Unit, -) { - - @Immutable - sealed class ImageState { - data object MobileWallet : ImageState() - data object Loading : ImageState() - data class Image( - val artwork: ArtworkUM, - ) : ImageState() - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt index 1e2e509d58..c86bd0befa 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.welcome.impl.ui.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -8,20 +9,15 @@ import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class WelcomeUM { + data object Empty : WelcomeUM() + data object Plain : WelcomeUM() data class SelectWallet( - val wallets: ImmutableList = persistentListOf(), + val wallets: ImmutableList = persistentListOf(), val showUnlockWithBiometricButton: Boolean = false, val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, val onUnlockWithBiometricClick: () -> Unit = {}, val addWalletClick: () -> Unit = {}, ) : WelcomeUM() - - data class EnterAccessCode( - val value: String = "", - val onUnlockWithBiometricClick: () -> Unit = {}, - val onValueChange: (String) -> Unit = {}, - val onBackClick: () -> Unit = {}, - ) : WelcomeUM() } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 03d9b1f740..f9ef6078d0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -30,7 +30,6 @@ androidxWorkManager = "2.9.0" # region Compose compose-runtime = "1.7.4" compose-foundation = "1.7.4" -compose-material = "1.7.4" compose-material3 = "1.3.1" compose-constraint = "1.0.1" compose-navigation = "2.7.7" @@ -73,14 +72,15 @@ viewBindingDelegate = "1.5.9" xmlShimmer = "1.1.3" zxingQrCode = "3.5.1" kotlinSerialization = "1.8.0" +kotlinDatetime = "0.6.2" arrow = "1.2.4" # 2.0.1 breaks the build reownCore = "1.1.2" reownWeb3 = "1.1.2" prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" chucker = "4.0.0" -mlKit-barcodeScanning = "17.2.0" -androidXCamera = "1.3.0" +mlKit-barcodeScanning = "17.3.0" +androidXCamera = "1.4.2" listenableFuture = "1.0" swipeRefreshLayout = "1.1.0" web3j = "4.12.3-SNAPSHOT" @@ -168,7 +168,6 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" } compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose-runtime" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } -compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } compose-constraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "compose-constraint" } compose-shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "compose-shimmer" } @@ -258,6 +257,7 @@ viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydeleg xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } +kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinDatetime" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } reownCore = { module = "com.reown:android-core", version.ref = "reownCore" } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index fb79857d96..f430a4dd17 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1140" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-518" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index c7512e7946..34d846e753 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -165,6 +165,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "zklink/test" -> Blockchain.ZkLinkNovaTestnet "pepecoin" -> Blockchain.Pepecoin "pepecoin/test" -> Blockchain.PepecoinTestnet + "hyperliquid" -> Blockchain.Hyperliquid + "hyperliquid/test" -> Blockchain.HyperliquidTestnet else -> null } } @@ -327,6 +329,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.ZkLinkNovaTestnet -> "zklink/test" Blockchain.Pepecoin -> "pepecoin" Blockchain.PepecoinTestnet -> "pepecoin/test" + Blockchain.Hyperliquid -> "hyperliquid" + Blockchain.HyperliquidTestnet -> "hyperliquid/test" } } @@ -430,6 +434,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll-ethereum" Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink-ethereum" Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network" + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> "hyperliquid" } } diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index f15198c725..12aeb18278 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -15,10 +15,8 @@ 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.ScanResponse -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index d7f00aa00a..896952a0e8 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -47,7 +47,7 @@ internal enum class BuildType( configFields = listOf( BuildConfigField.Environment(value = "dev"), BuildConfigField.LogEnabled(isEnabled = true), - BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = true), ), ), diff --git a/settings.gradle.kts b/settings.gradle.kts index 0550f19562..35d08f3116 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -183,6 +183,9 @@ include(":libs:tangem-sdk-api") include(":features:onboarding-v2:api") include(":features:onboarding-v2:impl") +include(":features:home:api") +include(":features:home:impl") + include(":features:referral:api") include(":features:referral:data") include(":features:referral:domain") @@ -257,15 +260,24 @@ include(":features:walletconnect:impl") include(":features:hot-wallet:api") include(":features:hot-wallet:impl") +include(":features:kyc:api") //TODO disable for release because of the permissions -// include(":features:kyc:api") // include(":features:kyc:impl") +include(":features:tangempay:main:api") +include(":features:tangempay:main:impl") + +include(":features:tangempay:details:api") +include(":features:tangempay:details:impl") + include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl") include(":features:welcome:api") include(":features:welcome:impl") + +include(":features:account:api") +include(":features:account:impl") // endregion Feature modules // region Domain modules @@ -273,6 +285,7 @@ include(":features:welcome:impl") include(":domain:models") include(":domain:legacy") +include(":domain:account") include(":domain:card") include(":domain:core") include(":domain:demo") @@ -330,6 +343,7 @@ include(":domain:wallet-manager:models") // endregion Domain modules // region Data modules +include(":data:account") include(":data:app-currency") include(":data:app-theme") include(":data:balance-hiding") diff --git a/tangem-android-tools b/tangem-android-tools index bc4cd43085..794a8187e6 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 +Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a