diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 68ed0730c5..17ee83e2e2 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,23 @@ 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) + implementation(projects.features.tokenRecieve.api) + implementation(projects.features.tokenRecieve.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -255,13 +267,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 +356,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/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/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index dcedc27851..4d82335720 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -154,4 +154,10 @@ internal object NFTDomainModule { ): ObserveAndClearNFTCacheIfNeedUseCase { return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository) } + + @Provides + @Singleton + fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase { + return GetNFTCurrencyUseCase(nftRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index 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..bdbe9037cc 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,17 +11,19 @@ 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 import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles @@ -46,6 +48,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, tokensFeatureToggles: TokensFeatureToggles, + stakingIdFactory: StakingIdFactory, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, @@ -54,6 +57,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -64,12 +68,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 +97,11 @@ internal object TokensDomainModule { @Singleton fun provideGetTokenListUseCase( currenciesRepository: CurrenciesRepository, - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetTokenListUseCase { return GetTokenListUseCase( currenciesRepository = currenciesRepository, - currenciesStatusesOperations = baseCurrenciesStatusesOperations, + currenciesStatusesOperations = currenciesStatusesOperations, ) } @@ -172,6 +178,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, tokensFeatureToggles: TokensFeatureToggles, + stakingIdFactory: StakingIdFactory, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( currenciesRepository = currenciesRepository, @@ -180,6 +187,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -190,12 +198,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 +369,9 @@ internal object TokensDomainModule { @Provides @Singleton fun provideGetWalletTotalBalanceUseCase( - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations) + return GetWalletTotalBalanceUseCase(currenciesStatusesOperations) } @Provides @@ -389,47 +399,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 +412,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 +427,11 @@ internal object TokensDomainModule { multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -472,6 +450,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { return WalletBalanceFetcher( @@ -481,6 +460,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) } @@ -490,4 +470,20 @@ internal object TokensDomainModule { fun provideGetAssetRequirementsUseCase(walletManagersFacade: WalletManagersFacade): GetAssetRequirementsUseCase { return GetAssetRequirementsUseCase(walletManagersFacade) } + + @Provides + @Singleton + fun provideGetViewedTokenReceiveWarningUseCase( + tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, + ): GetViewedTokenReceiveWarningUseCase { + return GetViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository) + } + + @Provides + @Singleton + fun provideSaveViewedTokenReceiveWarningUseCase( + tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, + ): SaveViewedTokenReceiveWarningUseCase { + return SaveViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository) + } } \ No newline at end of file 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..0b8802198c 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 @@ -9,9 +10,9 @@ import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.WalletAddressServiceRepository 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 +182,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 +205,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 @@ -210,4 +221,24 @@ internal object TransactionDomainModule { ): CreateNFTTransferTransactionUseCase { return CreateNFTTransferTransactionUseCase(transactionRepository) } + + @Provides + @Singleton + fun provideGetEnsNameUseCase( + walletManagersFacade: WalletManagersFacade, + walletAddressServiceRepository: WalletAddressServiceRepository, + ): GetEnsNameUseCase { + return GetEnsNameUseCase( + walletManagersFacade = walletManagersFacade, + walletAddressServiceRepository = walletAddressServiceRepository, + ) + } + + @Provides + @Singleton + fun provideGetReverseResolvedEnsAddressUseCase( + walletAddressServiceRepository: WalletAddressServiceRepository, + ): GetReverseResolvedEnsAddressUseCase { + return GetReverseResolvedEnsAddressUseCase(walletAddressServiceRepository) + } } \ No newline at end of file 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..911eededb2 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 @@ -23,7 +25,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") @Module @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { @@ -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, ) } @@ -261,4 +352,24 @@ internal object WalletsDomainModule { walletsRepository = walletsRepository, ) } + + @Provides + @Singleton + fun providesIsUpgradeWalletNotificationEnabledUseCase( + walletsRepository: WalletsRepository, + ): IsUpgradeWalletNotificationEnabledUseCase { + return IsUpgradeWalletNotificationEnabledUseCase( + walletsRepository = walletsRepository, + ) + } + + @Provides + @Singleton + fun providesDismissUpgradeWalletNotificationUseCase( + walletsRepository: WalletsRepository, + ): DismissUpgradeWalletNotificationUseCase { + return DismissUpgradeWalletNotificationUseCase( + walletsRepository = walletsRepository, + ) + } } \ No newline at end of file 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..9a80508e7b 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,21 @@ 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.UpgradeWalletComponent +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 +40,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 +52,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 +76,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,16 +95,25 @@ 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, private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, + private val upgradeWalletComponentFactory: UpgradeWalletComponent.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 +150,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 +319,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Home -> { createComponentChild( context = context, - params = Unit, + params = HomeComponent.Params(route.launchMode), componentFactory = homeComponentFactory, ) } @@ -385,6 +414,7 @@ internal class ChildFactory @Inject constructor( params = PushNotificationsParams( modelCallbacks = PushNotificationsModelCallbacksStub(), source = route.source, + nextRoute = AppRoute.Home(), ), componentFactory = pushNotificationsComponentFactory, ) @@ -438,6 +468,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, ) @@ -456,6 +488,15 @@ internal class ChildFactory @Inject constructor( componentFactory = createMobileWalletComponentFactory, ) } + is AppRoute.UpgradeWallet -> { + createComponentChild( + context = context, + params = UpgradeWalletComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = upgradeWalletComponentFactory, + ) + } is AppRoute.AddExistingWallet -> { createComponentChild( context = context, @@ -463,6 +504,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 +551,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..cfa1070c25 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, @@ -301,9 +311,29 @@ sealed class AppRoute(val path: String) : Route { @Serializable object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") + @Serializable + data class UpgradeWallet( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/upgrade_wallet/${userWalletId.stringValue}") + @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 +347,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/account/AccountIcon.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt new file mode 100644 index 0000000000..8a441922f5 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt @@ -0,0 +1,134 @@ +package com.tangem.common.ui.account + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color + +enum class AccountIconSize { + Default, Large, Medium, Small, ExtraSmall +} + +/** + * Displays an account icon that can either show a letter (derived from [name]) + * or a predefined vector resource (from [icon]). + * + * The background color is determined by the icon's [CryptoPortfolioIconUM.color], + * and the icon size, text style, and box modifier are adapted based on the given [size]. + * + * @param name The text reference used to resolve and display the first letter + * when [icon] is set to [CryptoPortfolioIcon.Icon.Letter]. + * @param icon The account icon definition, which can be a letter or a drawable resource. + * @param size The size of the icon, defined by [AccountIconSize]. + */ +@Composable +fun AccountIcon( + name: TextReference, + icon: CryptoPortfolioIconUM, + size: AccountIconSize, + modifier: Modifier = Modifier, +) { + val boxModifier = modifier.selectBoxModifier(size) + val iconSize = Modifier.selectIconSize(size) + val textStyle = when (size) { + AccountIconSize.Default -> TangemTheme.typography.h3 + AccountIconSize.Large -> TangemTheme.typography.h1 + AccountIconSize.Medium -> TangemTheme.typography.subtitle1 + AccountIconSize.Small -> TangemTheme.typography.subtitle2 + AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1 + } + Box( + contentAlignment = Alignment.Center, + modifier = boxModifier.background(icon.color.getUiColor()), + ) { + val icon = icon.value + val letter = name.resolveReference().firstOrNull() + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = letter?.uppercase() ?: "", + style = textStyle, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = iconSize, + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) { + AccountIconSize.Default -> this.size(20.dp) + AccountIconSize.Large -> this.size(40.dp) + AccountIconSize.Medium -> this.size(16.dp) + AccountIconSize.Small -> this.size(12.dp) + AccountIconSize.ExtraSmall -> this.size(8.dp) +} + +private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) { + AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp)) + AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp)) + AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp)) + AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp)) + AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp)) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_AccountIcon() { + TangemThemePreview { + Sample() + } +} + +@Composable +private fun Sample() { + val name = stringReference("Account Name") + Row( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall) + } + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall) + } + } +} + +object AccountIconPreviewData { + + fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM( + value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(), + color = Color.entries.random(), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt new file mode 100644 index 0000000000..d02fe62c15 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt @@ -0,0 +1,115 @@ +package com.tangem.common.ui.account + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.R +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.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Displays a row representing an account with an icon, title, and subtitle. + * + * The row consists of: + * - An [AccountIcon] on the left. + * - A column with the [title] and [subtitle] texts, which can be displayed in normal + * or reversed order depending on [isReverse]. + * + * The layout uses horizontal spacing between the icon and text, and vertical spacing + * between the title and subtitle. + * + * @param title The main text shown in the row, usually representing the account name. + * @param subtitle The secondary text, typically providing additional details about the account. + * @param icon The account icon definition, displayed using [AccountIcon]. + * @param isReverse If `true`, the [subtitle] is displayed above the [title]. + * Otherwise, the [title] is displayed above the [subtitle]. + */ +@Composable +fun AccountRow( + title: TextReference, + subtitle: TextReference, + icon: CryptoPortfolioIconUM, + modifier: Modifier = Modifier, + isReverse: Boolean = false, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + name = title, + icon = icon, + size = AccountIconSize.Default, + ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + if (isReverse) { + Subtitle(subtitle) + Title(title) + } else { + Title(title) + Subtitle(subtitle) + } + } + } +} + +@Composable +private fun Title(title: TextReference) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun Subtitle(subtitle: TextReference) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = subtitle.resolveReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Sample() + } +} + +@Composable +private fun Sample() { + val name = stringReference("Main account") + val info = stringReference("10 tokens in 2 networks") + val subtitle = resourceReference(R.string.account_form_name) + fun icon(letter: Boolean = false) = AccountIconPreviewData.randomAccountIcon(letter) + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + AccountRow(title = name, subtitle = info, icon = icon()) + AccountRow(title = name, subtitle = subtitle, icon = icon(), isReverse = true) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt index 14015c86b2..d97401c0a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt @@ -47,4 +47,7 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int { CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24 CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24 } -} \ No newline at end of file +} + +fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this) +fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt new file mode 100644 index 0000000000..cb8b2d22af --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt @@ -0,0 +1,15 @@ +package com.tangem.common.ui.account + +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color +import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon + +data class CryptoPortfolioIconUM( + val value: Icon, + val color: Color, +) { + constructor(domainModel: CryptoPortfolioIcon) : this( + value = domainModel.value, + color = domainModel.color, + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/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..1d4ff127d6 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 @@ -273,6 +273,7 @@ sealed class NotificationUM(val config: NotificationConfig) { iconResId: Int = R.drawable.ic_alert_circle_24, buttonsState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, + iconTint: NotificationConfig.IconTint = NotificationConfig.IconTint.Unspecified, ) : NotificationUM( config = NotificationConfig( title = title, @@ -280,6 +281,7 @@ sealed class NotificationUM(val config: NotificationConfig) { iconResId = iconResId, buttonsState = buttonsState, onCloseClick = onCloseClick, + iconTint = iconTint, ), ) @@ -373,5 +375,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..ad44dcf7dd 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), @@ -131,33 +139,59 @@ private fun NameAndInfo( ) } } - Text( - text = " $DOT ", - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - ) - AnimatedContent( - targetState = balance, - label = "Balance content", - ) { balance -> - val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance) + BalanceContent(balance) + } + } +} - if (balanceValue == null) { - TextShimmer( - style = TangemTheme.typography.caption2, - text = "aaaaa", - ) - } else { +@Composable +private fun BalanceContent(balance: UserWalletItemUM.Balance, modifier: Modifier = Modifier) { + AnimatedContent( + modifier = modifier, + targetState = balance, + label = "Balance content", + ) { balance -> + when (balance) { + UserWalletItemUM.Balance.Locked -> { + Icon( + modifier = Modifier + .padding(start = 4.dp, bottom = 2.dp, top = 2.dp) + .size(12.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_lock_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + UserWalletItemUM.Balance.NotShowing -> { + /** No balance and no dot */ + } + else -> { + Row { Text( - text = balanceValue, - style = TangemTheme.typography.caption2.applyBladeBrush( - isEnabled = isFlickering, - textColor = TangemTheme.colors.text.tertiary, - ), + text = " $DOT ", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, maxLines = 1, ) + + val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance) + + if (balanceValue == null) { + TextShimmer( + style = TangemTheme.typography.caption2, + text = "aaaaa", + ) + } else { + Text( + text = balanceValue, + style = TangemTheme.typography.caption2.applyBladeBrush( + isEnabled = isFlickering, + textColor = TangemTheme.colors.text.tertiary, + ), + maxLines = 1, + ) + } } } } @@ -179,6 +213,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) { @@ -225,9 +272,8 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair DASH_SIGN to false is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false - is UserWalletItemUM.Balance.Loading -> null to false - is UserWalletItemUM.Balance.Locked -> stringResourceSafe(R.string.common_locked) to false is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering + else -> null to false } } @@ -257,6 +303,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 +405,27 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { - private val artworkUMConverter = ArtworkUMConverter() + private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading override fun convert(value: UserWallet): UserWalletItemUM { return with(value) { @@ -45,16 +48,30 @@ 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 = artwork, + 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 getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { val text = when (userWallet) { is UserWallet.Cold -> { @@ -74,8 +91,9 @@ class UserWalletItemUMConverter( private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance { return when { - isBalanceHidden -> UserWalletItemUM.Balance.Hidden userWallet.isLocked -> UserWalletItemUM.Balance.Locked + authMode -> UserWalletItemUM.Balance.NotShowing + isBalanceHidden -> UserWalletItemUM.Balance.Hidden balance == null -> UserWalletItemUM.Balance.Loading else -> { when (balance) { 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..b31b4d5ddb 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, @@ -26,6 +28,8 @@ data class UserWalletItemUM( data object Hidden : Balance() + data object NotShowing : Balance() + data object Locked : Balance() data object Failed : Balance() @@ -54,6 +58,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..5879b1920e 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 { @@ -186,6 +192,10 @@ sealed class AnalyticsParam { Pending(value = "Pending"), } + enum class EnsStatus(val value: String) { + EMPTY("Empty"), FULL("Full") + } + companion object Key { const val BLOCKCHAIN = "Blockchain" const val TOKEN_PARAM = "Token" @@ -229,5 +239,12 @@ 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" + const val ENS = "ENS" + const val ENS_ADDRESS = "ENS Address" } } \ 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..bd1f7e6e67 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 @@ -9,7 +9,7 @@ }, { "name": "STAKING_TON_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "NFT_MEDIA_CONTENT_ENABLED", @@ -17,7 +17,7 @@ }, { "name": "STAKING_CARDANO_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "WALLET_CONNECT_REDESIGN_ENABLED", @@ -33,7 +33,7 @@ }, { "name": "SEND_VIA_SWAP_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "SWAP_REDESIGN_ENABLED", @@ -41,7 +41,7 @@ }, { "name": "SEND_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "WALLET_BALANCE_FETCHER_ENABLED", @@ -50,5 +50,17 @@ { "name": "HOT_WALLET_ENABLED", "version": "undefined" + }, + { + "name": "NFT_SEND_REDESIGN_ENABLED", + "version": "5.28.0" + }, + { + "name": "TANGEM_PAY_ENABLED", + "version": "undefined" + }, + { + "name": "NEW_TOKEN_RECEIVE_ENABLED", + "version": "5.28.0" } ] 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 2d6d08aff3..be6fe202e7 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 @@ -181,4 +145,20 @@ interface TangemTechApi { @POST("promo/v1/promo-codes/activate") suspend fun activatePromoCode(@Body body: PromocodeActivationBody): 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/TokenReceiveWarningModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TokenReceiveWarningModule.kt new file mode 100644 index 0000000000..c0aa4159dd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TokenReceiveWarningModule.kt @@ -0,0 +1,44 @@ +package com.tangem.datasource.di + +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.token.DefaultTokenReceiveWarningActionStore +import com.tangem.datasource.local.token.TokenReceiveWarningActionStore +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.setTypes +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object TokenReceiveWarningModule { + + @Provides + @Singleton + fun provideTokenReceiveWarningStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): TokenReceiveWarningActionStore { + return DefaultTokenReceiveWarningActionStore( + persistenceStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = setTypes(), + defaultValue = emptySet(), + ), + produceFile = { context.dataStoreFile(fileName = "token_receive_warnings_viewed") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + ) + } +} \ 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/DefaultTokenReceiveWarningActionStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultTokenReceiveWarningActionStore.kt new file mode 100644 index 0000000000..8be4bb214a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultTokenReceiveWarningActionStore.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.local.token + +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.firstOrNull + +internal class DefaultTokenReceiveWarningActionStore( + private val persistenceStore: DataStore>, +) : TokenReceiveWarningActionStore { + + override suspend fun getSync(): Set { + return persistenceStore.data.firstOrNull() ?: emptySet() + } + + override suspend fun store(symbol: String) { + persistenceStore.updateData { data -> data.plus(symbol) } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt new file mode 100644 index 0000000000..d7ef18221d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.local.token + +interface TokenReceiveWarningActionStore { + + suspend fun getSync(): Set + + suspend fun store(symbol: String) +} \ No newline at end of file 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/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/OptionsBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt new file mode 100644 index 0000000000..d355f8ff22 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.components.bottomsheets + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Generic options bottom sheet component + * + * @param config Bottom sheet configuration containing OptionsBottomSheetContent + * @param title Title text for the bottom sheet + * @param containerColor Background color of the bottom sheet + */ +@Composable +fun OptionsBottomSheet( + config: TangemBottomSheetConfig, + title: TextReference, + containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary, +) { + TangemBottomSheet( + config = config, + titleText = title, + containerColor = containerColor, + content = { content -> + OptionsBottomSheetContent(content = content) + }, + ) +} + +@Composable +private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.options.forEachIndexed { index, option -> + InputRowDefault( + text = option.label, + showDivider = index < content.options.size - 1, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = content.options.size - 1, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(option.key) }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OptionsBottomSheetPreview() { + TangemThemePreview { + OptionsBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = "option1", + label = TextReference.Str("First Option"), + ), + BottomSheetOption( + key = "option2", + label = TextReference.Str("Second Option"), + ), + BottomSheetOption( + key = "option3", + label = TextReference.Str("Third Option"), + ), + ), + onOptionClick = {}, + ), + ), + title = TextReference.Str("Select Option"), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt new file mode 100644 index 0000000000..02d3ab9479 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.components.bottomsheets + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param key Unique identifier for the option + * @param label Display text for the option + */ +data class BottomSheetOption( + val key: String, + val label: TextReference, +) + +/** + * @param options List of options to display + * @param onOptionClick Callback when an option is clicked, receives the option key + */ +data class OptionsBottomSheetContent( + val options: ImmutableList = persistentListOf(), + val onOptionClick: (String) -> Unit = {}, +) : TangemBottomSheetConfigContent \ No newline at end of file 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..3826cc503f 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 @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.bottomsheets.modal import android.content.res.Configuration +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -32,6 +33,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]. @@ -48,7 +50,7 @@ inline fun TangemModalBottomSheetWi noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit = {}, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current @@ -83,7 +85,7 @@ inline fun DefaultModalBottomSheetW noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { var isVisible by remember { mutableStateOf(value = config.isShown) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) @@ -117,7 +119,7 @@ inline fun PreviewModalBottomSheetW skipPartiallyExpanded: Boolean = true, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable BoxScope.(T) -> Unit, + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { BasicModalBottomSheetWithFooter( config = config, @@ -144,7 +146,7 @@ inline fun BasicModalBottomSheetWit noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, modifier: Modifier = Modifier, ) { val model = config.content as? T ?: return @@ -154,6 +156,26 @@ inline fun BasicModalBottomSheetWit val initial = 0 val scrollState = rememberScrollState(initial = initial) + val isKeyboardOpen by rememberIsKeyboardVisible() + val buttonHeight by animateDpAsState( + if (footer != null) { + 80.dp + } else { + 0.dp + }, + ) + // 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 +208,7 @@ inline fun BasicModalBottomSheetWit Column( modifier = Modifier .verticalScroll(state = scrollState) - .padding(bottom = TangemTheme.dimens.spacing80), + .padding(bottom = buttonHeight), ) { content(model) } @@ -199,10 +221,12 @@ inline fun BasicModalBottomSheetWit Box( modifier = Modifier .fillMaxWidth() - .height(80.dp) + .height(buttonHeight) .align(Alignment.BottomCenter), ) { - footer(model) + if (footer != null) { + footer(model) + } } } } @@ -219,6 +243,7 @@ inline fun BasicModalBottomSheetWit onBack = onBack, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -230,6 +255,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/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 4727ba9759..ff97530009 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme @@ -28,11 +29,16 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter * @param shouldDisplayNetwork specifies whether to display network badge */ @Composable -fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { +fun CurrencyIcon( + state: CurrencyIconState, + modifier: Modifier = Modifier, + shouldDisplayNetwork: Boolean = true, + iconSize: Dp = 36.dp, +) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) - .size(TangemTheme.dimens.size36) + .size(iconSize) when (state) { is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt new file mode 100644 index 0000000000..1aaed64244 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt @@ -0,0 +1,103 @@ +package com.tangem.core.ui.components.currency.icon + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.R +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 + +object CurrencyIconStateBuilder { + + fun build( + cryptoCurrency: CryptoCurrency, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState = when (cryptoCurrency) { + is CryptoCurrency.Coin -> fromCoin(cryptoCurrency, isGrayscale, showCustomBadge) + is CryptoCurrency.Token -> fromToken(cryptoCurrency, isGrayscale, showCustomBadge) + } + + private fun createCoinIcon( + url: String? = null, + @DrawableRes fallbackResId: Int = R.drawable.ic_empty_64, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = false, + ): CurrencyIconState.CoinIcon = CurrencyIconState.CoinIcon( + url = url, + fallbackResId = fallbackResId, + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + + private fun createTokenIcon( + url: String? = null, + @DrawableRes topBadgeIconResId: Int? = null, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = false, + fallbackTint: Color = Color.Black, + fallbackBackground: Color = Color.White, + ): CurrencyIconState.TokenIcon = CurrencyIconState.TokenIcon( + url = url, + topBadgeIconResId = topBadgeIconResId, + isGrayscale = isGrayscale, + fallbackTint = fallbackTint, + fallbackBackground = fallbackBackground, + showCustomBadge = showCustomBadge, + ) + + private fun createCustomTokenIcon( + tint: Color, + background: Color, + @DrawableRes topBadgeIconResId: Int, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState.CustomTokenIcon = CurrencyIconState.CustomTokenIcon( + tint = tint, + background = background, + topBadgeIconResId = topBadgeIconResId, + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + + private fun fromCoin( + coin: CryptoCurrency.Coin, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState.CoinIcon = createCoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = isGrayscale || coin.network.isTestnet, + showCustomBadge = coin.isCustom && showCustomBadge, + ) + + private fun fromToken( + token: CryptoCurrency.Token, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState { + val grayScale = isGrayscale || token.network.isTestnet + val background = token.tryGetBackgroundForTokenIcon(grayScale) + val tint = getTintForTokenIcon(background) + + return if (token.isCustom && token.iconUrl == null) { + createCustomTokenIcon( + tint = tint, + background = background, + topBadgeIconResId = token.networkIconResId, + isGrayscale = grayScale, + showCustomBadge = showCustomBadge, + ) + } else { + createTokenIcon( + url = token.iconUrl, + topBadgeIconResId = token.networkIconResId, + isGrayscale = grayScale, + fallbackTint = tint, + fallbackBackground = background, + showCustomBadge = token.isCustom && showCustomBadge, + ) + } + } +} \ No newline at end of file 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..a3e97923bd 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,13 +20,13 @@ 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 import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize @@ -35,6 +35,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 /** @@ -61,8 +62,8 @@ fun Notification( NotificationConfig.IconTint.Unspecified -> null NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention + NotificationConfig.IconTint.Warning -> TangemTheme.colors.icon.warning }, - iconSize: Dp = 20.dp, isEnabled: Boolean = true, ) { NotificationBaseContainer( @@ -76,7 +77,7 @@ fun Notification( MainContent( iconResId = config.iconResId, iconTint = iconTint, - iconSize = iconSize, + iconSize = config.iconSize, title = config.title, titleColor = titleColor, subtitle = config.subtitle, @@ -206,6 +207,7 @@ internal fun TextsBlock( text = titleText, color = titleColor, style = TangemTheme.typography.button, + modifier = Modifier.testTag(NotificationTestTags.TITLE), ) SpacerH(height = TangemTheme.dimens.spacing2) @@ -217,6 +219,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/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 4afe96144e..bd4d9dacef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -1,6 +1,8 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.TextReference /** @@ -26,6 +28,7 @@ data class NotificationConfig( val onCloseClick: (() -> Unit)? = null, val showArrowIcon: Boolean = onClick != null, val iconTint: IconTint = IconTint.Unspecified, + val iconSize: Dp = 20.dp, ) { sealed class ButtonsState { @@ -62,5 +65,6 @@ data class NotificationConfig( Unspecified, Accent, Attention, + Warning, } } \ No newline at end of file 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_copy_new_24.xml b/core/ui/src/main/res/drawable/ic_copy_new_24.xml new file mode 100644 index 0000000000..a073a2b686 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_copy_new_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_ens_36.xml b/core/ui/src/main/res/drawable/ic_ens_36.xml new file mode 100644 index 0000000000..29b19ea2c7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_ens_36.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml b/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml new file mode 100644 index 0000000000..071683ab08 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml @@ -0,0 +1,22 @@ + + + + + + + + + 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_knight_shield_24.xml b/core/ui/src/main/res/drawable/ic_knight_shield_24.xml new file mode 100644 index 0000000000..1a058ff367 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_knight_shield_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_mobile_security_24.xml b/core/ui/src/main/res/drawable/ic_mobile_security_24.xml new file mode 100644 index 0000000000..bbc69392d5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_security_24.xml @@ -0,0 +1,12 @@ + + + + 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_protect_24.xml b/core/ui/src/main/res/drawable/ic_protect_24.xml new file mode 100644 index 0000000000..5ad478076f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_protect_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml b/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml new file mode 100644 index 0000000000..173a8c1979 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml @@ -0,0 +1,9 @@ + + + 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/core/ui/src/main/res/drawable/ic_tangem_64.xml b/core/ui/src/main/res/drawable/ic_tangem_64.xml new file mode 100644 index 0000000000..6c39da496c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_64.xml @@ -0,0 +1,18 @@ + + + + + + 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..90800c1228 --- /dev/null +++ b/data/account/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.account" +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + + // region Project - Core + implementation(projects.core.datasource) + api(projects.core.utils) + // endregion + + // region Project - Domain + api(projects.domain.account) + api(projects.domain.models) + // endregion + + // region Project - Data + implementation(projects.data.common) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + + // region Other Dependencies + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + // endregion + + // region Test + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) + // endregion +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt new file mode 100644 index 0000000000..28cfd01b9e --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -0,0 +1,20 @@ +package com.tangem.data.account.converter + +import javax.inject.Inject + +/** + * Container for converter factories related to accounts. + * + * @property accountsListCF factory for creating an account list converter + * @property getWalletAccountsResponseCF factory for creating a wallet accounts response converter + * @property cryptoPortfolioCF factory for creating a crypto portfolio converter + * + * @constructor Creates an instance of the container with injected factories. + * +[REDACTED_AUTHOR] + */ +internal class AccountConverterFactoryContainer @Inject constructor( + val accountsListCF: AccountListConverter.Factory, + val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, + val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, +) \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt new file mode 100644 index 0000000000..ab8eef91a8 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt @@ -0,0 +1,33 @@ +package com.tangem.data.account.converter + +import arrow.core.getOrElse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +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.UserWalletId + +internal fun String.toAccountId(userWalletId: UserWalletId): AccountId { + return AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId).getOrElse { + error("Unable to create AccountId from value: $this. Cause: $it") + } +} + +internal fun String.toAccountName(): AccountName { + return AccountName(value = this).getOrElse { + error("Unable to create AccountName from value: $this. Cause: $it") + } +} + +internal fun WalletAccountDTO.toIcon(): CryptoPortfolioIcon { + return CryptoPortfolioIconConverter.convert( + value = CryptoPortfolioIconConverter.DataModel(icon = icon, color = iconColor), + ) +} + +internal fun Int.toDerivationIndex(): DerivationIndex { + return DerivationIndex(value = this).getOrElse { + error("Unable to create DerivationIndex from value: $this. Cause: $it") + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt new file mode 100644 index 0000000000..d4c993f59f --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -0,0 +1,46 @@ +package com.tangem.data.account.converter + +import arrow.core.getOrElse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.Converter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Converts a [GetWalletAccountsResponse] to an [AccountList] and vice versa + * + * @property userWallet the user wallet associated with the account list + * @param cryptoPortfolioConverterFactory factory to create [CryptoPortfolioConverter] instances + * +[REDACTED_AUTHOR] + */ +internal class AccountListConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, +) : Converter { + + private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy { + cryptoPortfolioConverterFactory.create(userWallet) + } + + override fun convert(value: GetWalletAccountsResponse): AccountList { + return AccountList( + userWallet = userWallet, + accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(), + totalAccounts = value.wallet.totalAccounts, + sortType = TokensSortTypeConverter.convert(value.wallet.sort), + groupType = TokensGroupTypeConverter.convert(value.wallet.group), + ) + .getOrElse { + error("Failed to convert GetWalletAccountsResponse to AccountList: $it") + } + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): AccountListConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt new file mode 100644 index 0000000000..072c82e3fc --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.converter.Converter + +/** + * Converts a [WalletAccountDTO] to an [ArchivedAccount] + * + * @param userWalletId the ID of the user wallet associated with the account + * +[REDACTED_AUTHOR] + */ +internal class ArchivedAccountConverter( + private val userWalletId: UserWalletId, +) : Converter { + + override fun convert(value: WalletAccountDTO): ArchivedAccount { + return ArchivedAccount( + accountId = value.id.toAccountId(userWalletId = userWalletId), + name = value.name.toAccountName(), + icon = value.toIcon(), + derivationIndex = value.derivationIndex.toDerivationIndex(), + tokensCount = value.totalTokens ?: error("Total tokens should not be null"), + networksCount = value.totalNetworks ?: error("Total networks should not be null"), + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt new file mode 100644 index 0000000000..bae26707a6 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.data.account.converter + +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.TwoWayConverter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Converts a [WalletAccountDTO] to an [Account.CryptoPortfolio] and vise versa + * + * @property userWallet the user wallet associated with the account list + * @property responseCryptoCurrenciesFactory factory to create crypto currencies from response tokens + * +[REDACTED_AUTHOR] + */ +internal class CryptoPortfolioConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + private val userTokensResponseFactory: UserTokensResponseFactory, +) : TwoWayConverter { + + override fun convert(value: WalletAccountDTO): Account.CryptoPortfolio { + val tokens = value.tokens ?: error("Tokens should not be null") + + return Account.CryptoPortfolio( + accountId = value.id.toAccountId(userWallet.walletId), + accountName = value.name.toAccountName(), + icon = value.toIcon(), + derivationIndex = value.derivationIndex.toDerivationIndex(), + cryptoCurrencies = if (tokens.isNotEmpty()) { + responseCryptoCurrenciesFactory.createCurrencies( + tokens = tokens, + userWallet = userWallet, + ).toSet() + } else { + emptySet() + }, + ) + } + + override fun convertBack(value: Account.CryptoPortfolio): WalletAccountDTO { + return WalletAccountDTO( + id = value.accountId.value, + name = value.accountName.value, + derivationIndex = value.derivationIndex.value, + icon = value.icon.value.name, + iconColor = value.icon.color.name, + tokens = value.cryptoCurrencies.map(userTokensResponseFactory::createResponseToken), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): CryptoPortfolioConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt new file mode 100644 index 0000000000..33b8b2ab20 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.account.converter + +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.utils.converter.Converter + +/** + * Converts a [CryptoPortfolioIconConverter.DataModel] to a [CryptoPortfolioIcon] + * +[REDACTED_AUTHOR] + */ +internal object CryptoPortfolioIconConverter : Converter { + + override fun convert(value: DataModel): CryptoPortfolioIcon { + return CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.valueOf(value.icon), + color = CryptoPortfolioIcon.Color.valueOf(value.color), + ) + } + + data class DataModel(val icon: String, val color: String) +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt new file mode 100644 index 0000000000..4ae6bf8b41 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.Converter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** +[REDACTED_AUTHOR] + */ +internal class GetWalletAccountsResponseConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + @Assisted val version: Int, + cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, +) : Converter { + + private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy { + cryptoPortfolioConverterFactory.create(userWallet) + } + + override fun convert(value: AccountList): GetWalletAccountsResponse { + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = version, + group = TokensGroupTypeConverter.convertBack(value.groupType), + sort = TokensSortTypeConverter.convertBack(value.sortType), + totalAccounts = value.totalAccounts, + ), + accounts = value.accounts + .filterIsInstance() + .map(cryptoPortfolioConverter::convertBack), + unassignedTokens = emptyList(), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet, version: Int): GetWalletAccountsResponseConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt new file mode 100644 index 0000000000..25c5307b84 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.utils.converter.Converter + +/** + * Converts an [AccountList] to a [SaveWalletAccountsResponse] + * +[REDACTED_AUTHOR] + */ +internal object SaveWalletAccountsResponseConverter : Converter { + + override fun convert(value: AccountList): SaveWalletAccountsResponse { + return SaveWalletAccountsResponse( + accounts = value.accounts + .filterIsInstance() + .map(::toDTO), + ) + } + + private fun toDTO(account: Account.CryptoPortfolio): WalletAccountDTO { + return WalletAccountDTO( + id = account.accountId.value, + name = account.accountName.value, + derivationIndex = account.derivationIndex.value, + icon = account.icon.value.name, + iconColor = account.icon.color.name, + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt new file mode 100644 index 0000000000..9a9b92af1b --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensGroupType +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts a [UserTokensResponse.GroupType] to a [TokensGroupType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object TokensGroupTypeConverter : TwoWayConverter { + + override fun convert(value: UserTokensResponse.GroupType): TokensGroupType { + return when (value) { + UserTokensResponse.GroupType.NETWORK -> TokensGroupType.NETWORK + UserTokensResponse.GroupType.NONE, + UserTokensResponse.GroupType.TOKEN, + -> TokensGroupType.NONE + } + } + + override fun convertBack(value: TokensGroupType): UserTokensResponse.GroupType { + return when (value) { + TokensGroupType.NONE -> UserTokensResponse.GroupType.NONE + TokensGroupType.NETWORK -> UserTokensResponse.GroupType.NETWORK + } + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt new file mode 100644 index 0000000000..91b2b2e88a --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensSortType +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts a [UserTokensResponse.SortType] to a [TokensSortType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object TokensSortTypeConverter : TwoWayConverter { + + override fun convert(value: UserTokensResponse.SortType): TokensSortType { + return when (value) { + UserTokensResponse.SortType.BALANCE -> TokensSortType.BALANCE + UserTokensResponse.SortType.MANUAL, + UserTokensResponse.SortType.MARKETCAP, + -> TokensSortType.NONE + } + } + + override fun convertBack(value: TokensSortType): UserTokensResponse.SortType { + return when (value) { + TokensSortType.NONE -> UserTokensResponse.SortType.MANUAL + TokensSortType.BALANCE -> UserTokensResponse.SortType.BALANCE + } + } +} \ 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/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt new file mode 100644 index 0000000000..a8ffea4d37 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.store + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +typealias AccountsResponseStore = DataStore + +/** + * Factory class for creating and managing instances of [AccountsResponseStore]. + * This class is responsible for creating a [DataStore] for each unique [UserWalletId]. + * + * @property context application context used to access the file system + * @property moshi moshi instance for JSON serialization and deserialization + * @property dispatchers coroutine dispatcher provider + * +[REDACTED_AUTHOR] + */ +internal class AccountsResponseStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + @OptIn(ExperimentalStdlibApi::class) + private val adapter by lazy { moshi.adapter() } + + private val createdDataStores = ConcurrentHashMap() + + /** + * Creates or retrieves an [AccountsResponseStore] for the given [UserWalletId]. + * + * @param userWalletId the unique identifier of the user's wallet + */ + fun create(userWalletId: UserWalletId): AccountsResponseStore { + return createdDataStores.computeIfAbsent(userWalletId) { + DataStoreFactory.create( + serializer = MoshiDataStoreSerializer(defaultValue = null, adapter = adapter), + produceFile = { context.dataStoreFile(fileName = "wallet_accounts_${userWalletId.stringValue}") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + } + + @VisibleForTesting + fun getAllStores(): Map = createdDataStores.toMap() + + @VisibleForTesting + fun clearStores() { + createdDataStores.clear() + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt new file mode 100644 index 0000000000..5670802776 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -0,0 +1,85 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +internal fun createWalletAccountDTO( + userWalletId: UserWalletId, + accountId: String? = null, + accountName: String? = null, + icon: String? = null, + iconColor: String? = null, + derivationIndex: Int? = null, + tokens: List? = emptyList(), +): WalletAccountDTO { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + return WalletAccountDTO( + id = accountId ?: mainAccount.accountId.value, + name = accountName ?: mainAccount.accountName.value, + derivationIndex = derivationIndex ?: mainAccount.derivationIndex.value, + icon = icon ?: mainAccount.icon.value.name, + iconColor = iconColor ?: mainAccount.icon.color.name, + tokens = tokens, + ) +} + +internal fun createCryptoPortfolio(userWalletId: UserWalletId): Account.CryptoPortfolio { + return Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) +} + +internal fun createGetWalletAccountsResponse( + userWalletId: UserWalletId, + groupType: UserTokensResponse.GroupType = UserTokensResponse.GroupType.NETWORK, + sortType: UserTokensResponse.SortType = UserTokensResponse.SortType.BALANCE, + accountId: String? = null, + accountName: String? = null, + icon: String? = null, + iconColor: String? = null, + derivationIndex: Int? = null, + tokens: List? = emptyList(), +): GetWalletAccountsResponse { + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = groupType, + sort = sortType, + totalAccounts = 1, + ), + accounts = buildList { + createWalletAccountDTO( + userWalletId = userWalletId, + accountId = accountId, + accountName = accountName, + icon = icon, + iconColor = iconColor, + derivationIndex = derivationIndex, + tokens = tokens, + ) + .let(::add) + }, + unassignedTokens = emptyList(), + ) +} + +internal fun createAccountList( + userWallet: UserWallet, + sortType: TokensSortType = TokensSortType.BALANCE, + groupType: TokensGroupType = TokensGroupType.NETWORK, +): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf(createCryptoPortfolio(userWallet.walletId)), + totalAccounts = 1, + sortType = sortType, + groupType = groupType, + ) + .getOrNull()!! +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt new file mode 100644 index 0000000000..8b32adb5e5 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -0,0 +1,159 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountListConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + private val cryptoPortfolioConverterFactory = mockk() + private val cryptoPortfolioConverter = mockk() + private val converter = AccountListConverter(userWallet, cryptoPortfolioConverterFactory) + + @BeforeAll + fun setupAll() { + every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + clearMocks(cryptoPortfolioConverter) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @Test + fun `cryptoPortfolioConverter throws exception`() { + // Arrange + val dto = createGetWalletAccountsResponse(userWallet.walletId) + val exception = IllegalStateException("Test exception") + + every { cryptoPortfolioConverter.convert(any()) } throws exception + + // Act + val actual = runCatching { converter.convert(dto) }.exceptionOrNull()!! + + // Asset + val expected = exception + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual.message).isEqualTo(expected.message) + } + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Arrange + if (model.expected.isSuccess) { + model.value.accounts.forEach { dto -> + val account = model.expected.getOrNull()!!.accounts + .firstOrNull { it.accountId.value == dto.id } as? Account.CryptoPortfolio + + every { cryptoPortfolioConverter.convert(dto) } returns account!! + } + } + + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.BALANCE, + groupType = UserTokensResponse.GroupType.NETWORK, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ), + ), + ), + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MANUAL, + groupType = UserTokensResponse.GroupType.TOKEN, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ), + ), + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MARKETCAP, + groupType = UserTokensResponse.GroupType.NONE, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ), + ), + ConvertModel( + value = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + totalAccounts = 1, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ), + expected = Result.failure( + IllegalStateException( + "Failed to convert GetWalletAccountsResponse to AccountList: EmptyAccountsList: " + + "The accounts list cannot be empty", + ), + ), + ), + ) + } + } + + data class ConvertModel( + val value: GetWalletAccountsResponse, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt new file mode 100644 index 0000000000..90831c1d3b --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt @@ -0,0 +1,144 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.ArchivedAccount +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.UserWalletId +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchivedAccountConverterTest { + + private val userWalletId = UserWalletId("011") + private val converter = ArchivedAccountConverter(userWalletId = userWalletId) + + @ParameterizedTest + @ProvideTestModels + fun convert(model: TestModel) { + // Act + val actual = runCatching { converter.convert(value = model.value) } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + TestModel( + value = createDTO(), + expected = Result.success(createDomain()), + ), + TestModel( + value = createDTO(accountId = "123"), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}", + ), + ), + ), + TestModel( + value = createDTO(name = ""), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}", + ), + ), + ), + TestModel( + value = createDTO(icon = "INVALID_ICON"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + TestModel( + value = createDTO(iconColor = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + TestModel( + value = createDTO(derivationIndex = -1), + expected = Result.failure( + IllegalStateException( + "Unable to create DerivationIndex from value: -1. " + + "Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1", + ), + ), + ), + TestModel( + value = createDTO(totalTokens = null), + expected = Result.failure( + IllegalStateException("Total tokens should not be null"), + ), + ), + TestModel( + value = createDTO(totalNetworks = null), + expected = Result.failure( + IllegalStateException("Total networks should not be null"), + ), + ), + ) + } + + private fun createDTO( + accountId: String = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027", + name: String = "Test Account", + icon: String = "Letter", + iconColor: String = "Azure", + derivationIndex: Int = 0, + totalTokens: Int? = 1, + totalNetworks: Int? = 1, + ): WalletAccountDTO { + return WalletAccountDTO( + id = accountId, + name = name, + derivationIndex = derivationIndex, + icon = icon, + iconColor = iconColor, + tokens = null, + totalTokens = totalTokens, + totalNetworks = totalNetworks, + ) + } + + private fun createDomain(): ArchivedAccount { + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex(0).getOrNull()!!), + name = "Test Account".toAccountName(), + derivationIndex = 0.toDerivationIndex(), + icon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.Azure, + ), + tokensCount = 1, + networksCount = 1, + ) + } + + data class TestModel( + val value: WalletAccountDTO, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt new file mode 100644 index 0000000000..f046bae96a --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt @@ -0,0 +1,155 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +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.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CryptoPortfolioConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk() + private val userTokensResponseFactory: UserTokensResponseFactory = mockk() + private val converter = CryptoPortfolioConverter( + userWallet = userWallet, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + userTokensResponseFactory = userTokensResponseFactory, + ) + + @BeforeEach + fun setupEach() { + clearMocks(responseCryptoCurrenciesFactory, userTokensResponseFactory) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId), + expected = Result.success(createCryptoPortfolio(userWalletId = userWallet.walletId)), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountId = "123"), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountName = ""), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, icon = "INVALID_ICON"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, iconColor = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, derivationIndex = -1), + expected = Result.failure( + IllegalStateException( + "Unable to create DerivationIndex from value: -1. " + + "Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, tokens = null), + expected = Result.failure( + IllegalStateException("Tokens should not be null"), + ), + ), + ) + } + } + + data class ConvertModel( + val value: WalletAccountDTO, + val expected: Result, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = converter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertBackModel( + value = createCryptoPortfolio(userWalletId = userWallet.walletId), + expected = createWalletAccountDTO(userWalletId = userWallet.walletId), + ), + ) + } + } + + data class ConvertBackModel( + val value: Account.CryptoPortfolio, + val expected: WalletAccountDTO, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt new file mode 100644 index 0000000000..c6563f9fc8 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.account.converter.CryptoPortfolioIconConverter.DataModel +import com.tangem.domain.models.account.CryptoPortfolioIcon +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CryptoPortfolioIconConverterTest { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: TestModel) { + // Act + val actual = runCatching { CryptoPortfolioIconConverter.convert(model.value) } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + TestModel( + value = DataModel(icon = "Letter", color = "Azure"), + expected = Result.success( + CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + ), + TestModel( + value = DataModel(icon = "INVALID_ICON", color = "Azure"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + TestModel( + value = DataModel(icon = "Letter", color = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + ) + } + + data class TestModel( + val value: DataModel, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt new file mode 100644 index 0000000000..b6dddb7aa5 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt @@ -0,0 +1,130 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetWalletAccountsResponseConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + private val cryptoPortfolioConverterFactory = mockk() + private val cryptoPortfolioConverter = mockk() + private val converter = GetWalletAccountsResponseConverter( + userWallet = userWallet, + version = 0, + cryptoPortfolioConverterFactory = cryptoPortfolioConverterFactory, + ) + + @BeforeAll + fun setupAll() { + every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + clearMocks(cryptoPortfolioConverter) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @Test + fun `cryptoPortfolioConverter throws exception`() { + // Arrange + val domain = createAccountList(userWallet = userWallet) + val exception = IllegalStateException("Test exception") + + every { cryptoPortfolioConverter.convertBack(any()) } throws exception + + // Act + val actual = runCatching { converter.convert(domain) }.exceptionOrNull()!! + + // Asset + val expected = exception + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual.message).isEqualTo(expected.message) + } + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Arrange + if (model.expected.isSuccess) { + model.value.accounts.forEach { domain -> + val dto = model.expected.getOrNull()!!.accounts.firstOrNull { it.id == domain.accountId.value } + + every { cryptoPortfolioConverter.convertBack(domain as Account.CryptoPortfolio) } returns dto!! + } + } + + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createAccountList( + userWallet = userWallet, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ), + expected = Result.success( + createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.BALANCE, + groupType = UserTokensResponse.GroupType.NETWORK, + ), + ), + ), + ConvertModel( + value = createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + expected = Result.success( + createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MANUAL, + groupType = UserTokensResponse.GroupType.NONE, + ), + ), + ), + ) + } + } + + data class ConvertModel( + val value: AccountList, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt new file mode 100644 index 0000000000..13ad114fe4 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt @@ -0,0 +1,50 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SaveWalletAccountsResponseConverterTest { + + @Test + fun convert() { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns UserWalletId("011") + } + + val accountList = AccountList( + userWallet = userWallet, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + totalAccounts = 1, + ) + .getOrNull()!! + + // Act + val actual = SaveWalletAccountsResponseConverter.convert(value = accountList) + + // Assert + val expected = SaveWalletAccountsResponse( + accounts = listOf( + WalletAccountDTO( + id = accountList.mainAccount.accountId.value, + name = accountList.mainAccount.accountName.value, + derivationIndex = accountList.mainAccount.derivationIndex.value, + icon = accountList.mainAccount.icon.value.name, + iconColor = accountList.mainAccount.icon.color.name, + ), + ), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt new file mode 100644 index 0000000000..62ba8b6f30 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt @@ -0,0 +1,85 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensGroupType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TokensGroupTypeConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = TokensGroupTypeConverter.convert(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = UserTokensResponse.GroupType.NETWORK, + expected = TokensGroupType.NETWORK, + ), + ConvertModel( + value = UserTokensResponse.GroupType.NONE, + expected = TokensGroupType.NONE, + ), + ConvertModel( + value = UserTokensResponse.GroupType.TOKEN, + expected = TokensGroupType.NONE, + ), + ) + } + } + + data class ConvertModel( + val value: UserTokensResponse.GroupType, + val expected: TokensGroupType, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = TokensGroupTypeConverter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertBackModel( + value = TokensGroupType.NETWORK, + expected = UserTokensResponse.GroupType.NETWORK, + ), + ConvertBackModel( + value = TokensGroupType.NONE, + expected = UserTokensResponse.GroupType.NONE, + ), + ) + } + } + + data class ConvertBackModel( + val value: TokensGroupType, + val expected: UserTokensResponse.GroupType, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt new file mode 100644 index 0000000000..e5476f5c49 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt @@ -0,0 +1,81 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensSortType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TokensSortTypeConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = TokensSortTypeConverter.convert(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + fun provideTestModels() = listOf( + ConvertModel( + value = UserTokensResponse.SortType.BALANCE, + expected = TokensSortType.BALANCE, + ), + ConvertModel( + value = UserTokensResponse.SortType.MANUAL, + expected = TokensSortType.NONE, + ), + ConvertModel( + value = UserTokensResponse.SortType.MARKETCAP, + expected = TokensSortType.NONE, + ), + ) + } + + data class ConvertModel( + val value: UserTokensResponse.SortType, + val expected: TokensSortType, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = TokensSortTypeConverter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + fun provideTestModels() = listOf( + ConvertBackModel( + value = TokensSortType.BALANCE, + expected = UserTokensResponse.SortType.BALANCE, + ), + ConvertBackModel( + value = TokensSortType.NONE, + expected = UserTokensResponse.SortType.MANUAL, + ), + ) + } + + data class ConvertBackModel( + val value: TokensSortType, + val expected: UserTokensResponse.SortType, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt new file mode 100644 index 0000000000..6c39f7181c --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt @@ -0,0 +1,91 @@ +package com.tangem.data.account.store + +import android.content.Context +import com.google.common.truth.Truth +import com.squareup.moshi.Moshi +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountsResponseStoreFactoryTest { + + private val context: Context = mockk() + private val moshi: Moshi = Moshi.Builder().build() + private val factory: AccountsResponseStoreFactory = AccountsResponseStoreFactory( + context = context, + moshi = moshi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @AfterEach + fun setup() { + clearMocks(context) + factory.clearStores() + } + + @Test + fun `creates new data store for unique userWalletId`() { + // Arrange + val userWalletId = UserWalletId("011") + val createdStore = factory.create(userWalletId = userWalletId) + + // Actual + val actual = factory.getAllStores() + + // Assert + Truth.assertThat(actual).containsExactly(userWalletId, createdStore) + } + + @Test + fun `reuses existing data store for same userWalletId`() { + val userWalletId = UserWalletId("011") + + // Arrange (first creation) + val firstStore = factory.create(userWalletId = userWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(userWalletId, firstStore) + + // Arrange (second creation) + val secondStore = factory.create(userWalletId = userWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + Truth.assertThat(actual2).containsExactly(userWalletId, secondStore) + Truth.assertThat(firstStore).isSameInstanceAs(secondStore) + } + + @Test + fun `creates separate data stores for different userWalletIds`() { + // Arrange (first creation) + val firstWalletId = UserWalletId("011") + val firstStore = factory.create(userWalletId = firstWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore) + + // Arrange (second creation) + val secondWalletId = UserWalletId("011") + val secondStore = factory.create(userWalletId = secondWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore) + Truth.assertThat(actual2).containsExactlyEntriesIn(expected) + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt index 4c2623149b..84b75233ac 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt @@ -2,8 +2,9 @@ package com.tangem.data.common.currency import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.models.currency.CryptoCurrency +import javax.inject.Inject -class UserTokensResponseFactory { +class UserTokensResponseFactory @Inject constructor() { fun createUserTokensResponse( currencies: List, 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/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 2cba8e6ef9..fe5dd5cc34 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -8,14 +8,17 @@ import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.repository.DefaultCurrenciesRepository import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository +import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.TokenReceiveWarningActionStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -83,4 +86,14 @@ internal object TokensDataModule { dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideTokenReceiveWarningsViewedRepository( + tokenReceiveWarningActionStore: TokenReceiveWarningActionStore, + ): TokenReceiveWarningsViewedRepository { + return DefaultTokenReceiveWarningsViewedRepository( + tokenReceiveWarningActionStore = tokenReceiveWarningActionStore, + ) + } } \ No newline at end of file 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/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt new file mode 100644 index 0000000000..0d31e13a55 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.data.tokens.repository + +import com.tangem.datasource.local.token.TokenReceiveWarningActionStore +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository + +internal class DefaultTokenReceiveWarningsViewedRepository( + private val tokenReceiveWarningActionStore: TokenReceiveWarningActionStore, +) : TokenReceiveWarningsViewedRepository { + + override suspend fun getViewedWarnings(): Set { + return tokenReceiveWarningActionStore.getSync() + } + + override suspend fun view(symbol: String) { + tokenReceiveWarningActionStore.store(symbol) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 48cef0138b..e22b05cb02 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.NameResolver import com.tangem.blockchain.common.ResolveAddressResult +import com.tangem.blockchain.common.ReverseResolveAddressResult import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -22,6 +23,40 @@ class DefaultWalletAddressServiceRepository( private val dispatchers: CoroutineDispatcherProvider, ) : WalletAddressServiceRepository { + override suspend fun getEns(userWalletId: UserWalletId, network: Network, address: String): String? { + return withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + walletManager?.wallet?.ens + } + } + + override suspend fun reverseResolveAddress( + userWalletId: UserWalletId, + network: Network, + address: String, + ): ReverseResolveAddressResult { + return withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + if (walletManager is NameResolver) { + walletManager.reverseResolve(address.toByteArray()) + } else { + ReverseResolveAddressResult.NotSupported + } + } + } + override suspend fun resolveAddress( userWalletId: UserWalletId, network: Network, 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 da3b78db9c..d8edb5dc18 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 @@ -22,6 +22,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 @@ -36,10 +37,11 @@ import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.collections.mutableSetOf typealias SeedPhraseNotificationsStatuses = Map -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, @@ -49,18 +51,85 @@ internal class DefaultWalletsRepository( private val authProvider: AuthProvider, ) : WalletsRepository { + private val upgradeWalletNotificationDisabled: MutableStateFlow> = + MutableStateFlow(mutableSetOf()) + override suspend fun shouldSaveUserWalletsSync(): Boolean { 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()) @@ -268,6 +337,16 @@ internal class DefaultWalletsRepository( } } + override fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow { + return upgradeWalletNotificationDisabled.map { + it.contains(userWalletId) + } + } + + override suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) { + upgradeWalletNotificationDisabled.update { it.plus(userWalletId) } + } + override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { tangemTechApi.updateWallet( walletId = walletId, 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 1a64234232..f305beb722 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 @@ -199,7 +199,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..89f7fd33c8 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -0,0 +1,204 @@ +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.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +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, + val sortType: TokensSortType, + val groupType: TokensGroupType, +) { + + /** 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, + sortType = this.sortType, + groupType = this.groupType, + ) + } + + /** + * 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, + sortType = this.sortType, + groupType = this.groupType, + ) + } + + /** + * 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, + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, + ): 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.accountName.value }.distinct().size + ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames } + + AccountList( + userWallet = userWallet, + accounts = accounts, + totalAccounts = totalAccounts, + sortType = sortType, + groupType = groupType, + ) + } + + /** + * 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, + cryptoCurrencies: Set = emptySet(), + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, + ): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf( + Account.CryptoPortfolio.createMainAccount( + userWalletId = userWallet.walletId, + cryptoCurrencies = cryptoCurrencies, + ), + ), + totalAccounts = 1, + sortType = sortType, + groupType = groupType, + ) + } + + 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..6a3866bc4b --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -0,0 +1,121 @@ +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, + icon = icon, + derivationIndex = derivationIndex, + cryptoCurrencies = emptySet(), + ) + } + + 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)) }, + ) + + // TODO: [REDACTED_JIRA] + return AccountList.empty( + userWallet = userWallet, + cryptoCurrencies = 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 { + + /** + * 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..f5dcec41aa --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -0,0 +1,123 @@ +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.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, + icon = this.icon, + derivationIndex = this.derivationIndex, + // TODO: [REDACTED_JIRA] + cryptoCurrencies = emptySet(), + ) + } + + 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..4451a5f50d --- /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(icon = 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..0c43198fa0 --- /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( + icon = 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..f30bc1dfda --- /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.accountName, + 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.accountName, + 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.accountName, + 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.accountName, + 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.accountName, + 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..1e442938fa --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,155 @@ +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 updatedAccountList = (accountList - account).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 updatedAccountList = (accountList - account).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..4c994aeb21 --- /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 com.google.common.truth.Truth +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex +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 = DerivationIndex(4) + 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..7c8f1a847f --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,206 @@ +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.accountName, + icon = account.icon, + derivationIndex = account.derivationIndex, + tokensCount = 1, + networksCount = 1, + ) + + val updatedAccountList = (accountList + account).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 = account.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.accountName, + icon = account.icon, + derivationIndex = account.derivationIndex, + tokensCount = 1, + networksCount = 1, + ) + + val updatedAccountList = (accountList + account).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..f1ba896a7c --- /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(icon = 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, icon = 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..9f452c4145 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.account.utils + +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()!!, + icon = icon, + derivationIndex = derivationIndex, + cryptoCurrencies = emptySet(), + ) +} \ 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/TokenReceiveConfig.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt new file mode 100644 index 0000000000..728496ef05 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.models + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +data class TokenReceiveConfig( + val shouldShowWarning: Boolean, + val cryptoCurrency: CryptoCurrency, + val userWalletId: UserWalletId, + val showMemoDisclaimer: Boolean, + val receiveAddress: List, + val tokenReceiveNotification: List = emptyList(), + val asset: Asset = Asset.Currency, +) + +@Serializable +data class ReceiveAddressModel( + val nameService: NameService, + val value: String, + val displayName: String, +) { + enum class NameService { + Default, Ens + } +} + +@Serializable +data class TokenReceiveNotification( + val title: Int, + val subtitle: Int, +) + +enum class Asset { + Currency, NFT +} \ No newline at end of file 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..b376be0db1 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -0,0 +1,173 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.either +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 accountName: 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 accountName name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property cryptoCurrencies set of tokens associated with the account + */ + @Serializable + data class CryptoPortfolio private constructor( + override val accountId: AccountId, + override val accountName: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: DerivationIndex, + val cryptoCurrencies: Set, + ) : 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() = cryptoCurrencies.size + + /** Number of distinct networks in the account */ + val networksCount: Int + get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size + + fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio { + return CryptoPortfolio( + accountId = this.accountId, + accountName = accountName, + icon = icon, + derivationIndex = this.derivationIndex, + cryptoCurrencies = this.cryptoCurrencies, + ) + } + + /** + * 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 icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account + */ + operator fun invoke( + accountId: AccountId, + name: String, + icon: CryptoPortfolioIcon, + derivationIndex: Int, + cryptoCurrencies: Set = emptySet(), + ): Either { + return either { + val accountName = AccountName(value = name).getOrElse { + raise(AccountNameError(cause = it)) + } + + val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse { + raise(DerivationIndexError(cause = it)) + } + + invoke( + accountId = accountId, + accountName = accountName, + icon = icon, + derivationIndex = derivationIndex, + cryptoCurrencies = cryptoCurrencies, + ) + } + } + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + accountName: AccountName, + icon: CryptoPortfolioIcon, + derivationIndex: DerivationIndex, + cryptoCurrencies: Set = emptySet(), + ): CryptoPortfolio { + return CryptoPortfolio( + accountId = accountId, + accountName = accountName, + icon = icon, + derivationIndex = derivationIndex, + cryptoCurrencies = cryptoCurrencies, + ) + } + + /** + * Creates a main account for the given user wallet ID + * + * @param userWalletId the ID of the user wallet + * @param cryptoCurrencies set of tokens associated with the account + */ + fun createMainAccount( + userWalletId: UserWalletId, + cryptoCurrencies: Set = emptySet(), + ): CryptoPortfolio { + val derivationIndex = DerivationIndex.Main + + return CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + accountName = AccountName.Main, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + cryptoCurrencies = cryptoCurrencies, + ) + } + } + } +} \ 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..3133206dd3 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +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, +) { + + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "AccountId.Error" + + data object Empty : Error { + override fun toString(): String = "$tag: Account ID cannot be blank" + } + + data object InvalidFormat : Error { + override fun toString(): String = "$tag: Account ID must be a 64-character hexadecimal string" + } + } + + companion object { + + private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") } + private val hexRegex = Regex("^[a-fA-F0-9]{64}$") + + /** + * Creates a unique account identifier for a crypto portfolio + * + * @param userWalletId the identifier of the user wallet + * @param value the unique string value representing the account + * + * @return an [Either] containing the [AccountId] on success, or an [Error] on failure + */ + fun forCryptoPortfolio(userWalletId: UserWalletId, value: String): Either = either { + ensure(value.isNotBlank()) { Error.Empty } + ensure(value.matches(hexRegex)) { Error.InvalidFormat } + + AccountId(value = value, userWalletId = userWalletId) + } + + /** + * 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/ens/EnsAddress.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/ens/EnsAddress.kt new file mode 100644 index 0000000000..d1ab13dded --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/ens/EnsAddress.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.models.ens + +sealed interface EnsAddress { + data class Address(val name: String) : EnsAddress + data class Error(val error: Exception) : EnsAddress + data object NotSupported : EnsAddress +} \ 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..91baaf2a76 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -0,0 +1,177 @@ +package com.tangem.domain.models.account + +import com.google.common.truth.Truth +import com.tangem.domain.models.account.Account.CryptoPortfolio +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.invoke( + accountId = mockk(), + name = name, + icon = mockk(), + derivationIndex = 0, + cryptoCurrencies = emptySet(), + ) + .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", + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + derivationIndex = derivationIndex.value, + cryptoCurrencies = emptySet(), + ) + .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, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + cryptoCurrencies = emptySet(), + ) + + 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, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + cryptoCurrencies = currencies, + ) + .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/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt new file mode 100644 index 0000000000..85f77d810a --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.nft + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.nft.repository.NFTRepository + +class GetNFTCurrencyUseCase(private val nftRepository: NFTRepository) { + + operator fun invoke(network: Network): CryptoCurrency = nftRepository.getNFTCurrency(network) +} \ No newline at end of file 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/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt new file mode 100644 index 0000000000..424ac7f642 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.tokens.model.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +sealed class TokenReceiveNewAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Token / Receive", event, params) { + + class ReceiveScreenOpened( + token: String, + blockchainName: String, + ensStatus: AnalyticsParam.EnsStatus, + ) : TokenReceiveNewAnalyticsEvent( + event = "Receive Screen Opened", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ENS to ensStatus.value, + ), + ) + + class ButtonCopyAddress( + token: String, + blockchainName: String, + ) : TokenReceiveNewAnalyticsEvent( + event = "Button - Copy Address", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ), + ) + + class ButtonCopyEns( + token: String, + blockchainName: String, + ) : TokenReceiveNewAnalyticsEvent( + event = "Button - ENS", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ), + ) + + class QrScreenOpened( + token: String, + blockchainName: String, + ) : TokenReceiveNewAnalyticsEvent( + event = "QR Screen Opened", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ), + ) +} \ 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/GetViewedTokenReceiveWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetViewedTokenReceiveWarningUseCase.kt new file mode 100644 index 0000000000..7665d02abc --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetViewedTokenReceiveWarningUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository + +class GetViewedTokenReceiveWarningUseCase( + private val tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, +) { + suspend operator fun invoke(): Set { + return tokenReceiveWarningsViewedRepository.getViewedWarnings() + } +} \ No newline at end of file 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/SaveViewedTokenReceiveWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/SaveViewedTokenReceiveWarningUseCase.kt new file mode 100644 index 0000000000..8827166743 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/SaveViewedTokenReceiveWarningUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository + +class SaveViewedTokenReceiveWarningUseCase( + private val tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, +) { + suspend operator fun invoke(symbol: String) { + tokenReceiveWarningsViewedRepository.view(symbol) + } +} \ No newline at end of file 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..70700beea8 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,26 @@ 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 { maybeNetworksStatuses -> + val networksStatuses = maybeNetworksStatuses.getOrNull() + + val currenciesAddresses = if (networksStatuses == null) { + emptyMap() + } else { + currencies.associate { currency -> + val networkStatus = networksStatuses.firstOrNull { it.network == currency.network } + + currency.id to extractAddress(networkStatus) + } + } + + getYieldsBalancesUpdates(userWalletId, currenciesAddresses) + }, flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -213,10 +233,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 +294,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 +395,27 @@ 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()) { currencyWithAddress -> + stakingIdFactory.create( + currencyId = currencyWithAddress.key, + defaultAddress = currencyWithAddress.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/repository/TokenReceiveWarningsViewedRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenReceiveWarningsViewedRepository.kt new file mode 100644 index 0000000000..02765acfe4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenReceiveWarningsViewedRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.repository + +interface TokenReceiveWarningsViewedRepository { + + suspend fun getViewedWarnings(): Set + + suspend fun view(symbol: String) +} \ No newline at end of file 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/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index 2dfc380480..f566a5a93b 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -1,15 +1,24 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.ResolveAddressResult +import com.tangem.blockchain.common.ReverseResolveAddressResult import com.tangem.domain.models.network.Network -import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.models.ParsedQrCode /** * Wallet address service repository. */ interface WalletAddressServiceRepository { + suspend fun getEns(userWalletId: UserWalletId, network: Network, address: String): String? + + suspend fun reverseResolveAddress( + userWalletId: UserWalletId, + network: Network, + address: String, + ): ReverseResolveAddressResult + suspend fun resolveAddress(userWalletId: UserWalletId, network: Network, address: String): ResolveAddressResult suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt new file mode 100644 index 0000000000..27a9fd1bfc --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.walletmanager.WalletManagersFacade + +class GetEnsNameUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val walletAddressServiceRepository: WalletAddressServiceRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network, address: String): String? { + if (network.nameResolvingType != Network.NameResolvingType.ENS) { + return null + } + + val addresses = walletManagersFacade.getAddresses(userWalletId, network) + + val isOwnAddress = addresses.any { it.value == address } + if (!isOwnAddress) { + return null + } + + return walletAddressServiceRepository.getEns( + userWalletId = userWalletId, + network = network, + address = address, + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt new file mode 100644 index 0000000000..93f053818e --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.blockchain.common.ReverseResolveAddressResult +import com.tangem.domain.models.ens.EnsAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.WalletAddressServiceRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.supervisorScope + +class GetReverseResolvedEnsAddressUseCase(private val walletAddressServiceRepository: WalletAddressServiceRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + addresses: List, + ): List { + return supervisorScope { + val addressesDeferred = addresses.map { address -> + async { + val result = walletAddressServiceRepository.reverseResolveAddress( + userWalletId = userWalletId, + network = network, + address = address, + ) + mapReverseResolveResult(result) + } + } + addressesDeferred.awaitAll() + } + } + + private fun mapReverseResolveResult(reverseResolveAddressResult: ReverseResolveAddressResult): EnsAddress { + return when (reverseResolveAddressResult) { + is ReverseResolveAddressResult.Error -> EnsAddress.Error(reverseResolveAddressResult.error) + ReverseResolveAddressResult.NotSupported -> EnsAddress.NotSupported + is ReverseResolveAddressResult.Resolved -> EnsAddress.Address(reverseResolveAddressResult.name) + } + } +} \ 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 2a2f93d29c..7513e6e3bf 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 @@ -13,10 +13,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) @@ -49,6 +59,10 @@ interface WalletsRepository { suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) + fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow + + suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) + @Throws suspend fun setWalletName(walletId: String, walletName: String) 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/DismissUpgradeWalletNotificationUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt new file mode 100644 index 0000000000..2aeb755fba --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class DismissUpgradeWalletNotificationUseCase( + private val walletsRepository: WalletsRepository, +) { + suspend operator fun invoke(userWalletId: UserWalletId) { + walletsRepository.dismissUpgradeWalletNotification(userWalletId) + } +} \ No newline at end of file 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/IsUpgradeWalletNotificationEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt new file mode 100644 index 0000000000..d38fec4fe2 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.flow.Flow + +class IsUpgradeWalletNotificationEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return walletsRepository.isUpgradeWalletNotificationEnabled(userWalletId) + } +} \ No newline at end of file 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..95554e161b --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -0,0 +1,70 @@ +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.resourceReference +import com.tangem.core.ui.extensions.wrappedList +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 = resourceReference(R.string.account_archived_recover_dialog_title), + message = resourceReference( + id = R.string.account_archived_recover_dialog_description, + formatArgs = wrappedList(account.accountName.value), + ), + 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..bd72960062 --- /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.common.ui.account.CryptoPortfolioIconUM +import com.tangem.core.ui.extensions.TextReference +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: TextReference, + val accountIconUM: 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..a563f952d3 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -0,0 +1,182 @@ +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.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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountRow +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.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.features.account.archived.entity.AccountArchivedUM +import com.tangem.features.account.archived.entity.ArchivedAccountUM +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), + ) { + AccountRow( + title = item.accountName, + subtitle = item.tokensInfo, + icon = item.accountIconUM, + modifier = Modifier.weight(1f), + ) + + 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() = AccountIconPreviewData.randomAccountIcon() + val accountName = stringReference("Account name") + + val firstList = List(10) { + ArchivedAccountUM( + accountId = it.toString(), + accountName = accountName, + accountIconUM = 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/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt new file mode 100644 index 0000000000..03934e051e --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -0,0 +1,203 @@ +package com.tangem.features.account.createedit + +import com.tangem.common.ui.account.toDomain +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.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.accountName + 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.accountName.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 { cause -> + handleError( + error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, + message = cause.toString(), + params = mapOf( + "userWalletId" to userWalletId.stringValue, + "cause" to cause.toString(), + ), + ) + + return@launch + } + } + } + + private fun handleError( + error: AccountFeatureError, + message: String? = null, + params: Map = mapOf(), + ) { + val exception = IllegalStateException("$error. Cause: $message") + + 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..4b4365ba95 --- /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.common.ui.account.CryptoPortfolioIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.account.CryptoPortfolioIcon +import kotlinx.collections.immutable.ImmutableList + +internal 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..24b69681c9 --- /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.common.ui.account.toUM +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 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.accountName.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..639b44419e --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -0,0 +1,342 @@ +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.AccountIcon +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountIconSize +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.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() + AccountIcons(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( + name = stringReference(account.name), + icon = account.portfolioIcon, + size = AccountIconSize.Large, + ) + 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) + } +} + +@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 AccountIcons(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 = AccountIconPreviewData.randomAccountIcon() + 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 = AccountIconPreviewData.randomAccountIcon(letter = true) + val accountName = "Main account" + val second = AccountCreateEditUM( + title = stringReference("Edit account"), + onCloseClick = {}, + account = Account( + portfolioIcon = portfolioIcon, + name = accountName, + 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..0e40b6b39d --- /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.common.ui.account.toUM +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.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.accountName.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..799c295838 --- /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.common.ui.account.CryptoPortfolioIconUM + +internal 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..7f3c86da48 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -0,0 +1,187 @@ +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.AccountIconPreviewData +import com.tangem.common.ui.account.AccountRow +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.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.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), + ) { + AccountRow( + title = stringReference(state.accountName), + subtitle = resourceReference(R.string.account_form_name), + icon = state.accountIcon, + modifier = Modifier.weight(1f), + isReverse = true, + ) + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.common_edit), + onClick = state.onAccountEditClick, + ), + ) + } +} + +@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 { + val accountName = "Main" + var portfolioIcon = AccountIconPreviewData.randomAccountIcon() + val first = AccountDetailsUM( + onCloseClick = {}, + onAccountEditClick = {}, + onManageTokensClick = {}, + onArchiveAccountClick = {}, + accountName = accountName, + accountIcon = portfolioIcon, + ) + add(first) + portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true) + 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/build.gradle.kts b/features/details/impl/build.gradle.kts index e44ec003ea..8f41e0f6c1 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) + implementation(projects.features.createWalletSelection.api) + implementation(projects.features.hotWallet.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..f217f837ce 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -11,4 +12,5 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, ) \ No newline at end of file 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..bfdc9b284b 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 @@ -6,12 +6,19 @@ 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.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -20,22 +27,28 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class UserWalletListModel @Inject constructor( userWalletsFetcherFactory: UserWalletsFetcher.Factory, shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val router: Router, private val messageSender: UiMessageSender, - private val userWalletSaver: UserWalletSaver, override val dispatchers: CoroutineDispatcherProvider, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletSaver: UserWalletSaver, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = false, + authMode = false, onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, ) @@ -44,7 +57,8 @@ internal class UserWalletListModel @Inject constructor( userWallets = persistentListOf(), isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, - onAddNewWalletClick = ::addUserWallet, + onAddNewWalletClick = ::onAddNewWalletClick, + addWalletBottomSheet = TangemBottomSheetConfig.Empty, ), ) @@ -53,8 +67,9 @@ internal class UserWalletListModel @Inject constructor( flow = userWalletsFetcher.userWallets, flow2 = shouldSaveUserWalletsUseCase(), flow3 = isWalletSavingInProgress, - transform = ::updateState, - ).launchIn(modelScope) + ) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress -> + updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress) + }.launchIn(modelScope) } private fun updateState( @@ -65,7 +80,7 @@ internal class UserWalletListModel @Inject constructor( value.copy( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, - addNewWalletText = if (shouldSaveUserWallets) { + addNewWalletText = if (shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled) { resourceReference(R.string.user_wallet_list_add_button) } else { resourceReference(R.string.scan_card_settings_button) @@ -73,7 +88,64 @@ internal class UserWalletListModel @Inject constructor( ) } - private fun addUserWallet() = withProgress(isWalletSavingInProgress) { - userWalletSaver.scanAndSaveUserWallet(modelScope) + private fun onAddNewWalletClick() { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismissAddWalletBottomSheet, + content = createAddWalletBottomSheetContent(), + ), + ) + } + } else { + withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet(modelScope) + } + } + } + + private fun dismissAddWalletBottomSheet() { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false), + ) + } + } + + private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent { + return OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + dismissAddWalletBottomSheet() + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + }, + ) + } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index ec27900868..bea3f8c35d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -15,9 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig 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.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent @@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M onClick = state.onAddNewWalletClick, ) } + + AddWalletBottomSheet(state.addWalletBottomSheet) } @Composable @@ -94,6 +100,15 @@ private fun AddWalletButton( } } +@Composable +private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) 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/UpgradeWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt new file mode 100644 index 0000000000..8a9b7f6c14 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.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 UpgradeWalletComponent : 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..4596985307 --- /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..c41dba03e8 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 @@ -61,7 +61,7 @@ internal fun OptionBlock( color = backgroundColor, shape = TangemTheme.shapes.roundedCornersXMedium, ) - .conditional(onClick != null) { + .conditional(onClick != null && enabled) { onClick?.let { clickableSingle(onClick = it) } ?: Modifier } .padding(16.dp), 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/upgradewallet/DefaultUpgradeWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt new file mode 100644 index 0000000000..2f0e91d1de --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.hotwallet.upgradewallet + +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.UpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.ui.UpgradeWalletContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultUpgradeWalletComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: UpgradeWalletComponent.Params, +) : UpgradeWalletComponent, AppComponentContext by context { + + private val model: UpgradeWalletModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + UpgradeWalletContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : UpgradeWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: UpgradeWalletComponent.Params, + ): DefaultUpgradeWalletComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt new file mode 100644 index 0000000000..bc03e54e43 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -0,0 +1,41 @@ +package com.tangem.features.hotwallet.upgradewallet + +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.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM +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 UpgradeWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, +) : Model() { + + internal val uiState: StateFlow + field = MutableStateFlow( + UpgradeWalletUM( + onBackClick = { router.pop() }, + onBuyTangemWalletClick = ::onBuyTangemWalletClick, + onScanDeviceClick = ::onScanDeviceClick, + ), + ) + + private fun onBuyTangemWalletClick() { + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanDeviceClick() { + // TODO [REDACTED_TASK_KEY] + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt new file mode 100644 index 0000000000..3beb13bcab --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.upgradewallet.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.UpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.DefaultUpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.UpgradeWalletModel +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 UpgradeWalletModule { + + @Binds + fun bindUpgradeWalletComponentFactory(impl: DefaultUpgradeWalletComponent.Factory): UpgradeWalletComponent.Factory + + @Binds + @IntoMap + @ClassKey(UpgradeWalletModel::class) + fun bindUpgradeWalletModel(model: UpgradeWalletModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt new file mode 100644 index 0000000000..8f700a27b9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt @@ -0,0 +1,7 @@ +package com.tangem.features.hotwallet.upgradewallet.entity + +internal data class UpgradeWalletUM( + val onBackClick: () -> Unit, + val onBuyTangemWalletClick: () -> Unit, + val onScanDeviceClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt new file mode 100644 index 0000000000..2ae6ae5d0c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt @@ -0,0 +1,161 @@ +package com.tangem.features.hotwallet.upgradewallet.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +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.R +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +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.upgradewallet.entity.UpgradeWalletUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier + .statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.onBackClick), + title = TextReference.EMPTY, + ) + Column( + modifier = Modifier + .weight(1f) + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + ) { + Icon( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(R.drawable.ic_tangem_64), + contentDescription = null, + tint = Color.Unspecified, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 20.dp, + end = 16.dp, + ), + text = stringResourceSafe(R.string.hw_upgrade_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 32.dp), + title = stringResourceSafe(R.string.hw_upgrade_key_migration_title), + description = stringResourceSafe(R.string.hw_upgrade_key_migration_description), + iconRes = R.drawable.ic_mobile_security_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_upgrade_funds_access_title), + description = stringResourceSafe(R.string.hw_upgrade_funds_access_description), + iconRes = R.drawable.ic_knight_shield_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_upgrade_general_security_title), + description = stringResourceSafe(R.string.hw_upgrade_general_security_description), + iconRes = R.drawable.ic_protect_24, + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + SecondaryButton( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.details_buy_wallet), + onClick = state.onBuyTangemWalletClick, + ) + PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.hw_upgrade_scan_device), + onClick = state.onScanDeviceClick, + iconResId = R.drawable.ic_tangem_24, + ) + } + } +} + +@Composable +private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + ) { + Icon( + modifier = Modifier + .padding(horizontal = 12.dp), + painter = painterResource(iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(top = 4.dp), + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewUpgradeWalletContent() { + TangemThemePreview { + UpgradeWalletContent( + state = UpgradeWalletUM( + onBackClick = {}, + onBuyTangemWalletClick = {}, + onScanDeviceClick = {}, + ), + ) + } +} \ 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..3bc262d1a9 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,15 @@ 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 recoveryPhraseOption: LabelUM?, + val googleDriveOption: LabelUM?, val googleDriveStatus: BackupStatus, 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..9996c7fb7a 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,10 +1,24 @@ 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 @@ -18,6 +32,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 +41,85 @@ internal class WalletBackupModel @Inject constructor( field = MutableStateFlow( WalletBackupUM( onBackClick = { router.pop() }, - recoveryPhraseStatus = BackupStatus.NoBackup, + recoveryPhraseOption = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveOption = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), googleDriveStatus = BackupStatus.ComingSoon, - onRecoveryPhraseClick = { }, + 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( + recoveryPhraseOption = 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, + ) + }, + googleDriveOption = 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..671c8dd89a 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.recoveryPhraseOption?.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.googleDriveOption?.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,49 @@ private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider: private class WalletBackupUMProvider : CollectionPreviewParameterProvider( collection = listOf( WalletBackupUM( - recoveryPhraseStatus = BackupStatus.NoBackup, + recoveryPhraseOption = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveOption = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), googleDriveStatus = BackupStatus.ComingSoon, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, + backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = BackupStatus.NoBackup, + recoveryPhraseOption = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveOption = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), googleDriveStatus = BackupStatus.NoBackup, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, + backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = BackupStatus.Done, + recoveryPhraseOption = LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ), + googleDriveOption = LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ), googleDriveStatus = BackupStatus.Done, 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..61d498c799 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -16,6 +16,8 @@ dependencies { api(projects.features.markets.api) api(projects.features.onramp.api) api(projects.features.sendV2.api) + api(projects.features.tokenRecieve.api) + api(projects.features.wallet.api) /* Data */ implementation(projects.data.common) @@ -39,6 +41,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.settings) implementation(projects.domain.notifications.models) + implementation(projects.domain.transaction) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. @@ -50,7 +53,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/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 73841ec05d..8f3e1d6da7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -5,12 +5,20 @@ import androidx.compose.runtime.Stable 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.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -19,20 +27,41 @@ import dagger.assisted.AssistedInject internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: MarketsPortfolioComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : AppComponentContext by context, MarketsPortfolioComponent { private val model: MarketsPortfolioModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + override fun setTokenNetworks(networks: List) = model.setTokenNetworks(networks) override fun setNoNetworksAvailable() = model.setNoNetworksAvailable() @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() MyPortfolio(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + @AssistedFactory interface Factory : MarketsPortfolioComponent.Factory { override fun create( 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/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index feef86486d..49b7fead68 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -56,7 +55,7 @@ internal class AddToPortfolioBSContentUMFactory( portfolioUIData: PortfolioUIData, selectedWallet: UserWallet?, alreadyAddedNetworks: Set?, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { return (currentState ?: TangemBottomSheetConfig.Empty).copy( isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, @@ -110,7 +109,7 @@ internal class AddToPortfolioBSContentUMFactory( } private fun UserWallet.toSelectedUserWalletItemUM( - artwork: ArtworkModel? = null, + artwork: UserWalletItemUM.ImageState? = null, portfolioData: PortfolioData, balance: TotalFiatBalance?, ): UserWalletItemUM { @@ -128,7 +127,7 @@ internal class AddToPortfolioBSContentUMFactory( isShow: Boolean, portfolioData: PortfolioData, selectedWalletId: UserWalletId, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( isShown = isShow, 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..5b6bd84ca2 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 @@ -2,6 +2,9 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -15,35 +18,41 @@ 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.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.ArtworkModel +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig 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.isMultiCurrency -import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class MarketsPortfolioModel @Inject constructor( @@ -57,18 +66,17 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val getCardImageUseCase: GetCardImageUseCase, private val addToPortfolioManager: AddToPortfolioManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, + private val userWalletImageFetcher: UserWalletImageFetcher, ) : Model() { val state: StateFlow get() = _state private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - private val loadedArtworks: HashMap = hashMapOf() - private val artworksState: MutableStateFlow> = MutableStateFlow(hashMapOf()) - private val loadArtworksMutex = Mutex() - private val params = paramsContainer.require() private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( token = params.token, @@ -80,6 +88,8 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val currentAppCurrency = getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } @@ -128,7 +138,11 @@ internal class MarketsPortfolioModel @Inject constructor( tokenActionsHandler = tokenActionsIntentsFactory.create( currentAppCurrency = Provider { currentAppCurrency.value }, updateTokenReceiveBSConfig = { updateBlock -> - updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) { + updateTokensState { + it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) + } + } }, onHandleQuickAction = { handledAction -> analyticsEventHandler.send( @@ -137,6 +151,9 @@ internal class MarketsPortfolioModel @Inject constructor( blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name, ), ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + configureReceiveAddresses(handledAction) + } }, ), updateTokens = { updateBlock -> @@ -175,38 +192,33 @@ internal class MarketsPortfolioModel @Inject constructor( private fun subscribeOnStateUpdates() { combine( - flow = loadPortfolioData(params.token.id), + flow = loadPortfolioDataWithArtworks(params.token.id), flow2 = getPortfolioUIDataFlow(), - flow3 = artworksState, - transform = factory::create, + transform = { pair, portfolioUIData -> + val (portfolioData, artworks) = pair + factory.create(portfolioData, portfolioUIData, artworks) + }, ) .onEach { _state.value = it } .launchIn(modelScope) } - private fun loadPortfolioData(currencyRawId: CryptoCurrency.RawID): Flow { - portfolioDataLoader.load(currencyRawId).onEach { - loadArtworks(it.walletsWithCurrencies.keys.toList()) - }.also { return it } - } + private fun loadPortfolioDataWithArtworks( + currencyRawId: CryptoCurrency.RawID, + ): Flow>> { + val wallets = Channel>() + val portfolioFlow = portfolioDataLoader + .load(currencyRawId) + .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - private fun loadArtworks(wallets: List) { - modelScope.launch { - loadArtworksMutex.withLock { - wallets.filterIsInstance().forEach { wallet -> - if (!loadedArtworks.containsKey(wallet.walletId)) { - val artwork = getCardImageUseCase( - cardId = wallet.cardId, - manufacturerName = wallet.scanResponse.card.manufacturer.name, - firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(), - cardPublicKey = wallet.scanResponse.card.cardPublicKey, - ) - loadedArtworks[wallet.walletId] = artwork - artworksState.emit(loadedArtworks) - } - } - } - } + val artworksFlow = wallets.receiveAsFlow() + .distinctUntilChanged() + .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } + + return combine( + flow = portfolioFlow, + flow2 = artworksFlow, + ) { portfolioData, artworks -> portfolioData to artworks } } private fun getPortfolioUIDataFlow(): Flow { @@ -359,4 +371,51 @@ internal class MarketsPortfolioModel @Inject constructor( block(tokensState) } } + + private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { + when (quickAction.action) { + TokenActionsBSContentUM.Action.Receive -> { + val addresses = quickAction.cryptoCurrencyData.status.value.networkAddress ?: return + val cryptoCurrency = quickAction.cryptoCurrencyData.status.currency + modelScope.launch { + val ensName = getEnsNameUseCase.invoke( + userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, + network = cryptoCurrency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})", + ), + ) + } + } + val tokenConfig = TokenReceiveConfig( + shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, + showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + bottomSheetNavigation.activate(tokenConfig) + } + } + else -> Unit + } + } } \ No newline at end of file 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..44fd810a9a 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 @@ -1,9 +1,9 @@ package com.tangem.features.markets.portfolio.impl.model +import com.tangem.common.ui.userwallet.state.UserWalletItemUM 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 @@ -36,7 +36,7 @@ internal class MyPortfolioUMFactory( fun create( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - artworks: HashMap, + artworks: Map, ): MyPortfolioUM { val addToPortfolioData = portfolioUIData.addToPortfolioData @@ -89,7 +89,7 @@ internal class MyPortfolioUMFactory( private fun createAddToPortfolioBSConfig( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { val selectedWallet = portfolioData.walletsWithCurrencies.keys .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } 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..438a815cc3 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /** Api */ implementation(projects.features.nft.api) + implementation(projects.features.tokenRecieve.api) /** Core modules */ implementation(projects.core.configToggles) @@ -34,6 +35,8 @@ dependencies { implementation(projects.domain.nft.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction) + implementation(projects.domain.tokens) /** Common */ implementation(projects.common.ui) @@ -52,7 +55,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/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 5c4874dd5e..6aa2bc4a8f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -22,6 +22,7 @@ import com.tangem.features.nft.details.info.NFTDetailsInfoComponent import com.tangem.features.nft.entity.NFTSendSuccessListener import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.traits.NFTAssetTraitsComponent +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,6 +36,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( @Assisted private val params: NFTComponent.Params, private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory, nftSendSuccessListener: NFTSendSuccessListener, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -134,6 +136,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( walletName = params.walletName, onBackClick = ::onChildBack, ), + tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) private fun getDetailsComponent( 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/NFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt index f445c95fb7..e4bd9a112e 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt @@ -4,29 +4,58 @@ 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.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.receive.ui.NFTReceive +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedInject internal class NFTReceiveComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by context { private val model: NFTReceiveModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NFTReceive(state, modifier) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class Params( val userWalletId: UserWalletId, val walletName: String, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index fc2df6f5c2..632959d7e8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.nft.receive.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -12,12 +14,20 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.Asset +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.TokenReceiveNotification import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase +import com.tangem.domain.nft.GetNFTCurrencyUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.nft.GetNFTNetworksUseCase import com.tangem.domain.nft.analytics.NFTAnalyticsEvent +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.features.nft.impl.R import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.receive.entity.NFTReceiveUM @@ -25,6 +35,8 @@ import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheet import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* @@ -43,11 +55,17 @@ internal class NFTReceiveModel @Inject constructor( private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, private val messageSender: UiMessageSender, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, + private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase, paramsContainer: ParamsContainer, ) : Model() { private val params: NFTReceiveComponent.Params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _state = MutableStateFlow( value = NFTReceiveUM( onBackClick = params.onBackClick, @@ -138,14 +156,23 @@ internal class NFTReceiveModel @Inject constructor( when (val value = networkStatus.value) { is NetworkStatus.Verified -> { - _state.update { - ShowReceiveBottomSheetTransformer( - network = network, - networkAddress = value.address, - onDismissBottomSheet = ::onReceiveBottomSheetDismiss, - onCopyClick = { text -> onCopyClick(text, network) }, - onShareClick = { text -> onShareClick(text, network) }, - ).transform(it) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + bottomSheetNavigation.activate( + configuration = configureReceiveAddresses( + addresses = value.address, + network = network, + ), + ) + } else { + _state.update { + ShowReceiveBottomSheetTransformer( + network = network, + networkAddress = value.address, + onDismissBottomSheet = ::onReceiveBottomSheetDismiss, + onCopyClick = { text -> onCopyClick(text, network) }, + onShareClick = { text -> onShareClick(text, network) }, + ).transform(it) + } } } is NetworkStatus.MissedDerivation, @@ -166,4 +193,56 @@ internal class NFTReceiveModel @Inject constructor( analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ShareAddress(network.name)) shareManager.shareText(text = text) } + + private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig { + val cryptoCurrency = getNFTCurrencyUseCase.invoke(network) + + val ensName = getEnsNameUseCase.invoke( + userWalletId = params.userWalletId, + network = network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = cryptoCurrency.symbol, + ), + ) + } + } + + val notifications = buildList { + if (BlockchainUtils.isSolana(network.rawId)) { + add( + TokenReceiveNotification( + title = R.string.nft_receive_unsupported_types, + subtitle = R.string.nft_receive_unsupported_types_description, + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = Asset.NFT.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = params.userWalletId, + showMemoDisclaimer = false, + receiveAddress = receiveAddresses, + tokenReceiveNotification = notifications, + asset = Asset.NFT, + ) + } } \ No newline at end of file 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..d6abeb7356 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.features.manageTokens.api) implementation(projects.features.biometry.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.tokenRecieve.api) /** Core modules */ implementation(projects.core.configToggles) @@ -49,6 +50,7 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.onramp) + implementation(projects.domain.transaction) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) @@ -62,7 +64,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..092e847893 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,12 +20,12 @@ 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.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute import dagger.assisted.Assisted @@ -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/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index ded3238c83..9879c7252b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -2,8 +2,8 @@ package com.tangem.features.onboarding.v2.twin.impl import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier 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 @@ -42,7 +42,6 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { BackHandler { model.onBack() } - val state by model.uiState.collectAsStateWithLifecycle() OnboardingTwin( 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..07e8e30c81 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,7 +121,15 @@ 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)) + if (feeItem !is FeeItem.Custom) { + onDoneClick() + } } override fun onCustomFeeValueChange(index: Int, value: String) { @@ -132,6 +149,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 +178,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..d9aea80f8c 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,18 +79,22 @@ 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 = { - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - enabled = state.isPrimaryButtonEnabled, - text = stringResourceSafe(R.string.common_done), - onClick = feeSelectorIntents::onDoneClick, - ) + footer = if (state.selectedFeeItem is FeeItem.Custom) { + { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + enabled = state.isPrimaryButtonEnabled, + text = stringResourceSafe(R.string.common_done), + onClick = feeSelectorIntents::onDoneClick, + ) + } + } else { + null }, ) } @@ -115,7 +119,6 @@ private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDis } } -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun FeeSelectorItems( state: FeeSelectorUM.Content, @@ -141,114 +144,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 +156,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 +287,7 @@ private fun ExpandedCustomFeeItems( showDivider = false, modifier = Modifier .background( - color = TangemTheme.colors.background.action, + color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium, ), ) @@ -495,11 +419,25 @@ 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, ), - selectedFeeItem = customFeeItem, + selectedFeeItem = FeeItem.Slow( + fee = Fee.Common( + Amount( + value = BigDecimal("0.01"), + blockchain = Blockchain.Ethereum, + ), + ), + ), feeExtraInfo = FeeExtraInfo( isFeeApproximate = true, isFeeConvertibleToFiat = true, 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..c486cce7a4 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 @@ -3,11 +3,12 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS 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 @@ -23,6 +24,7 @@ internal sealed class SendAnalyticEvents( val feeType: AnalyticsParam.FeeType, val blockchain: String, val nonceNotEmpty: Boolean, + private val ensStatus: AnalyticsParam.EnsStatus, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = mapOf( @@ -30,6 +32,21 @@ internal sealed class SendAnalyticEvents( FEE_TYPE to feeType.value, BLOCKCHAIN to blockchain, NONCE to nonceNotEmpty.toString().capitalize(), + ENS_ADDRESS to when (ensStatus) { + AnalyticsParam.EnsStatus.EMPTY -> false.toString() + AnalyticsParam.EnsStatus.FULL -> true.toString() + }, + ), + ) + + 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/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index ab9490ce77..ad01b7a5c9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -28,6 +28,7 @@ internal class SendAnalyticHelper @Inject constructor( feeType = feeType, blockchain = cryptoCurrency.network.name, nonceNotEmpty = feeSelectorUM.nonce != null, + ensStatus = getEnsStatus(sendUM), ), ) analyticsEventHandler.send( @@ -52,4 +53,14 @@ internal class SendAnalyticHelper @Inject constructor( else -> Basic.TransactionSent.MemoType.Null } } + + private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus { + val blockchainAddressForEns = + (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.blockchainAddress + return if (blockchainAddressForEns != null) { + AnalyticsParam.EnsStatus.FULL + } else { + AnalyticsParam.EnsStatus.EMPTY + } + } } \ 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..45ace3d940 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 @@ -45,6 +47,7 @@ import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2 import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger @@ -60,6 +63,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 +92,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 +144,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 +241,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)) @@ -336,6 +350,10 @@ internal class NFTSendConfirmModel @Inject constructor( updateTransactionStatus(txData) sendBalanceUpdater.scheduleUpdates() nftSendAnalyticHelper.nftSendSuccessAnalytics(cryptoCurrency, uiState.value) + if (uiState.value.isRedesignEnabled) { + params.callback.onResult(uiState.value) + params.onSendTransaction() + } }, ) } @@ -376,13 +394,23 @@ internal class NFTSendConfirmModel @Inject constructor( } _uiState.update { it.copy( - confirmUM = NFTSendConfirmationNotificationsTransformer( - feeUM = uiState.value.feeUM, - analyticsEventHandler = analyticsEventHandler, - cryptoCurrency = cryptoCurrencyStatus.currency, - analyticsCategoryName = analyticsCategoryName, - appCurrency = params.appCurrency, - ).transform(uiState.value.confirmUM), + confirmUM = if (uiState.value.isRedesignEnabled) { + NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = uiState.value.feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + appCurrency = params.appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + } else { + NFTSendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + analyticsCategoryName = analyticsCategoryName, + appCurrency = params.appCurrency, + ) + }.transform(uiState.value.confirmUM), ) } } @@ -395,13 +423,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 +449,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 +474,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..b5bd158810 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 @@ -62,8 +62,10 @@ internal class NFTSendConfirmationNotificationsTransformer( val feeUM = feeUM as? FeeUM.Content val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee ?: return TextReference.EMPTY + val fiatFeeValue = feeUM.rate?.let { fee.amount.value?.multiply(it) } + val fiatFee = formatFooterFiatFee( - amount = fee.amount, + amount = fee.amount.copy(value = fiatFeeValue), isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, isFeeApproximate = feeUM.isFeeApproximate, appCurrency = appCurrency, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt new file mode 100644 index 0000000000..d69431a549 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.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.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.impl.R +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class NFTSendConfirmationNotificationsTransformerV2( + private val feeSelectorUM: FeeSelectorUM, + private val analyticsEventHandler: AnalyticsEventHandler, + private val cryptoCurrency: CryptoCurrency, + private val appCurrency: AppCurrency, + private val analyticsCategoryName: String, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + val state = prevState as? ConfirmUM.Content ?: return prevState + val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content ?: return prevState + return state.copy( + sendingFooter = getSendingFooterText(), + notifications = buildList { + addTooHighNotification(feeSelectorUM) + addTooLowNotification(feeSelectorUM) + }.toPersistentList(), + ) + } + + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { + if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) { + add(NotificationUM.Warning.FeeTooLow) + analyticsEventHandler.send( + CommonSendAnalyticEvents.NoticeTransactionDelays( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), + ) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) { + val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } + + private fun getSendingFooterText(): TextReference { + val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content + val fee = feeSelectorUM?.selectedFeeItem?.fee ?: return TextReference.EMPTY + + val fiatFeeValue = feeSelectorUM.feeFiatRateUM?.rate?.let { fee.amount.value?.multiply(it) } + + val fiatFee = formatFooterFiatFee( + amount = fee.amount.copy(value = fiatFeeValue), + isFeeConvertibleToFiat = feeSelectorUM.feeFiatRateUM != null, + isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate, + appCurrency = appCurrency, + ) + + return if (fee is Fee.Tron) { + getTronTokenFeeSendingText( + fee = fee, + fiatFee = fiatFee, + fiatSending = resourceReference(R.string.common_nft), + ) + } else { + resourceReference( + id = if (feeSelectorUM.feeFiatRateUM != null) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList( + resourceReference(R.string.common_nft), + fiatFee, + ), + ) + } + } +} \ No newline at end of file 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/NFTSendConfirmNotificationsTransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmNotificationsTransformersComparisonTest.kt new file mode 100644 index 0000000000..1fea9dd3ae --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmNotificationsTransformersComparisonTest.kt @@ -0,0 +1,822 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2 +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 io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2 + +class NFTSendConfirmNotificationsTransformersComparisonTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN equivalent input data WHEN both transformers transform THEN they produce equal ConfirmUM`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createTestFeeUM() + val feeSelectorUMV2 = createTestFeeSelectorUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.notifications.size).isEqualTo(contentV2.notifications.size) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.isSending).isEqualTo(contentV2.isSending) + assertThat(contentV1.showTapHelp).isEqualTo(contentV2.showTapHelp) + } + + @Test + fun `GIVEN fee too low WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createFeeTooLowUM() + val feeSelectorUMV2 = createFeeTooLowUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isTrue() + assertThat(contentV2.isPrimaryButtonEnabled).isTrue() + assertThat(contentV1.notifications).hasSize(1) + assertThat(contentV2.notifications).hasSize(1) + assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + + // Verify analytics event was sent for both transformers + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN fee too high WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createFeeTooHighUM() + val feeSelectorUMV2 = createFeeTooHighUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isTrue() + assertThat(contentV2.isPrimaryButtonEnabled).isTrue() + assertThat(contentV1.notifications).hasSize(1) + assertThat(contentV2.notifications).hasSize(1) + assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + + val tooHighV1 = contentV1.notifications.first() as NotificationUM.Warning.TooHigh + val tooHighV2 = contentV2.notifications.first() as NotificationUM.Warning.TooHigh + assertThat(tooHighV1.value).isEqualTo(tooHighV2.value) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + } + + @Test + fun `GIVEN fee both too high and too low WHEN both transformers transform THEN they produce equal notifications`() = + runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createFeeTooHighAndTooLowUM() + val feeSelectorUMV2 = createFeeTooHighAndTooLowUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isTrue() + assertThat(contentV2.isPrimaryButtonEnabled).isTrue() + assertThat(contentV1.notifications).hasSize(2) + assertThat(contentV2.notifications).hasSize(2) + + assertThat(contentV1.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + assertThat(contentV1.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(contentV2.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + assertThat(contentV2.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN normal fee WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createNormalFeeUM() + val feeSelectorUMV2 = createNormalFeeSelectorUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications).isEmpty() + assertThat(contentV2.notifications).isEmpty() + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createTestFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createTestFeeSelectorUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Market(fee), + ), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooLowUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooLowUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighUMV2(): FeeSelectorUMV2.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighAndTooLowUMV2(): FeeSelectorUMV2.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createNormalFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeSelectorUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Market(fee), + ), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt new file mode 100644 index 0000000000..e7f065638a --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt @@ -0,0 +1,381 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +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 io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale + +class NFTSendConfirmationNotificationsTransformerTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeUM: FeeUM = mockk(relaxed = true) + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM = ConfirmUM.Empty + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN fee UM not content WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeUM: FeeUM = FeeUM.Empty() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM.Content = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest { + // GIVEN + val feeUM = createNormalFeeUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).isEmpty() + assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter) + } + + @Test + fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest { + // GIVEN + val feeUM = createFeeTooHighUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + } + + @Test + fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { + // GIVEN + val feeUM = createFeeTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + verify { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest { + // GIVEN + val feeUM = createFeeTooHighAndTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(2) + assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf(), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooLowUM(): FeeUM.Content { + val fee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt new file mode 100644 index 0000000000..e79d598f5e --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt @@ -0,0 +1,450 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2 +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale + +class NFTSendConfirmationNotificationsTransformerV2Test { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeSelectorUM: FeeSelectorUM = mockk(relaxed = true) + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM = ConfirmUM.Empty + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN fee selector not content WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest { + // GIVEN + val feeSelectorUM = createNormalFeeSelectorUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).isEmpty() + assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter) + } + + @Test + fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooHighUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + } + + @Test + fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + verify { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooHighAndTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(2) + assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf(FeeItem.Market(fee)), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighUM(): FeeSelectorUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooLowUM(): FeeSelectorUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeSelectorUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file 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/SendConfirmNotificationsTransformersComparisonTest.kt similarity index 99% rename from features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt rename to features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmNotificationsTransformersComparisonTest.kt index 79cba32b78..4c7c95d94f 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/SendConfirmNotificationsTransformersComparisonTest.kt @@ -27,7 +27,7 @@ import java.util.Locale import com.tangem.domain.tokens.model.Amount as DomainAmount import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2 -class TransformersComparisonTest { +class SendConfirmNotificationsTransformersComparisonTest { private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) @@ -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/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/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/token-recieve/api/.gitignore b/features/token-recieve/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/token-recieve/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/token-recieve/api/build.gradle.kts b/features/token-recieve/api/build.gradle.kts new file mode 100644 index 0000000000..5b31ee2af9 --- /dev/null +++ b/features/token-recieve/api/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.tokenrecieve.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Common */ + implementation(projects.common.ui) + + /** Domain */ + implementation(projects.domain.models) + +} \ No newline at end of file diff --git a/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt new file mode 100644 index 0000000000..c37becdbb2 --- /dev/null +++ b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.tokenreceive + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.TokenReceiveConfig + +interface TokenReceiveComponent : ComposableBottomSheetComponent { + + data class Params( + val config: TokenReceiveConfig, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Params): TokenReceiveComponent + } +} \ No newline at end of file diff --git a/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt new file mode 100644 index 0000000000..62175072dd --- /dev/null +++ b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt @@ -0,0 +1,6 @@ +package com.tangem.features.tokenreceive + +interface TokenReceiveFeatureToggle { + + val isNewTokenReceiveEnabled: Boolean +} \ No newline at end of file diff --git a/features/token-recieve/impl/.gitignore b/features/token-recieve/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/token-recieve/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts new file mode 100644 index 0000000000..b1b8d7b048 --- /dev/null +++ b/features/token-recieve/impl/build.gradle.kts @@ -0,0 +1,58 @@ +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.tokenreceive.impl" +} + +dependencies { + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + implementation(deps.lifecycle.compose) + implementation(deps.kotlin.serialization) + implementation(deps.decompose.ext.compose) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** Common */ + implementation(projects.common.ui) + + /** Core modules */ + implementation(projects.common.routing) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.decompose) + implementation(projects.core.res) + implementation(projects.core.configToggles) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + + /** Feature Apis */ + implementation(projects.features.tokenRecieve.api) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt new file mode 100644 index 0000000000..209317d97b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt @@ -0,0 +1,11 @@ +package com.tangem.features.tokenreceive + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTokenReceiveFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) : TokenReceiveFeatureToggle { + + override val isNewTokenReceiveEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("NEW_TOKEN_RECEIVE_ENABLED") +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt new file mode 100644 index 0000000000..83b0cf3b6f --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -0,0 +1,131 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +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.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.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.model.TokenReceiveModel +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +import com.tangem.features.tokenreceive.ui.TokenReceiveContentSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultTokenReceiveComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: TokenReceiveComponent.Params, +) : TokenReceiveComponent, AppComponentContext by appComponentContext { + + private val model: TokenReceiveModel = getOrCreateModel(params) + private val innerRouter = InnerRouter( + stackNavigation = model.stackNavigation, + popCallback = { onChildBack() }, + ) + + private val contentStack = childStack( + key = "TokenReceiveFlowStack", + source = model.stackNavigation, + serializer = TokenReceiveRoutes.serializer(), + initialConfiguration = getInitialConfig(model.params.config.shouldShowWarning), + handleBackButton = false, + childFactory = ::screenChild, + ) + + @Composable + override fun BottomSheet() { + val content by contentStack.subscribeAsState() + val currentRoute = content.active.configuration + + TokenReceiveContentSheet( + route = currentRoute, + onCloseClick = ::dismiss, + onBackClick = ::onChildBack, + contentStack = content, + ) + } + + override fun dismiss() { + model.params.onDismiss() + } + + private fun onChildBack() { + when (contentStack.value.active.configuration) { + is TokenReceiveRoutes.QrCode -> model.stackNavigation.pop() + TokenReceiveRoutes.ReceiveAssets, + TokenReceiveRoutes.Warning, + -> dismiss() + } + } + + private fun screenChild( + config: TokenReceiveRoutes, + componentContext: ComponentContext, + ): ComposableContentComponent { + val appComponentContext = childByContext( + componentContext = componentContext, + router = innerRouter, + ) + return when (config) { + is TokenReceiveRoutes.QrCode -> TokenReceiveQrCodeComponent( + appComponentContext = appComponentContext, + params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams( + network = model.params.config.cryptoCurrency.network.name, + address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"), + callback = model, + onDismiss = ::dismiss, + id = config.addressId, + ), + ) + TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent( + appComponentContext = appComponentContext, + params = TokenReceiveAssetsComponent.TokenReceiveAssetsParams( + addresses = model.state.value.addresses, + callback = model, + onDismiss = ::dismiss, + notificationConfigs = model.state.value.notificationConfigs, + showMemoDisclaimer = model.params.config.showMemoDisclaimer, + fullName = model.params.config.cryptoCurrency.network.name, + tokenName = model.getTokenName(), + ), + ) + TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent( + appComponentContext = appComponentContext, + params = TokenReceiveWarningComponent.TokenReceiveWarningParams( + iconState = model.state.value.iconState, + callback = model, + onDismiss = ::dismiss, + network = model.params.config.cryptoCurrency.network, + ), + ) + } + } + + private fun getInitialConfig(shouldShowWarning: Boolean): TokenReceiveRoutes { + return if (shouldShowWarning) { + TokenReceiveRoutes.Warning + } else { + TokenReceiveRoutes.ReceiveAssets + } + } + + @AssistedFactory + interface Factory : TokenReceiveComponent.Factory { + override fun create( + context: AppComponentContext, + params: TokenReceiveComponent.Params, + ): DefaultTokenReceiveComponent + } +} + +internal interface TokenReceiveModelCallback : + TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback, + TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback, + TokenReceiveWarningComponent.TokenReceiveWarningModelCallback \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt new file mode 100644 index 0000000000..7ece6e356d --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.notifications.NotificationUM +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.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel +import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal class TokenReceiveAssetsComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveAssetsParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveAssetsModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveAssetsContent(assetsUM = state) + } + + internal interface TokenReceiveAssetsModelCallback { + fun onQrCodeClick(id: Int) + fun onCopyClick(id: Int) + } + + data class TokenReceiveAssetsParams( + val notificationConfigs: ImmutableList, + val addresses: ImmutableMap, + val callback: TokenReceiveAssetsModelCallback, + val onDismiss: () -> Unit, + val showMemoDisclaimer: Boolean, + val fullName: String, + val tokenName: String, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt new file mode 100644 index 0000000000..8191f9a208 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tokenreceive.component + +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.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel +import com.tangem.features.tokenreceive.ui.TokenReceiveQrCodeContent + +internal class TokenReceiveQrCodeComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveQrCodeParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveQrCodeModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveQrCodeContent(qrCodeUM = state) + } + + internal interface TokenReceiveQrCodeModelCallback { + fun onCopyClick(id: Int) + fun onShareClick(address: String) + } + + data class TokenReceiveQrCodeParams( + val id: Int, + val network: String, + val address: ReceiveAddress, + val callback: TokenReceiveQrCodeModelCallback, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt new file mode 100644 index 0000000000..46ce303258 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.tokenreceive.component + +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.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.network.Network +import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel +import com.tangem.features.tokenreceive.ui.TokenReceiveWarningContent + +internal class TokenReceiveWarningComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveWarningParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveWarningModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveWarningContent(warningUM = state) + } + + internal interface TokenReceiveWarningModelCallback { + fun onWarningAcknowledged() + } + + data class TokenReceiveWarningParams( + val iconState: CurrencyIconState, + val callback: TokenReceiveWarningModelCallback, + val onDismiss: () -> Unit, + val network: Network, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt new file mode 100644 index 0000000000..44cfdf6d8b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt @@ -0,0 +1,68 @@ +package com.tangem.features.tokenreceive.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.tokenreceive.DefaultTokenReceiveFeatureToggle +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.features.tokenreceive.component.DefaultTokenReceiveComponent +import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel +import com.tangem.features.tokenreceive.model.TokenReceiveModel +import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel +import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel +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 object FeatureToggleModule { + + @Provides + @Singleton + fun provideTokenReceiveFeatureToggle(featureTogglesManager: FeatureTogglesManager): TokenReceiveFeatureToggle { + return DefaultTokenReceiveFeatureToggle( + featureTogglesManager = featureTogglesManager, + ) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultTokenReceiveComponent.Factory): TokenReceiveComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(TokenReceiveModel::class) + fun bindsTokenReceiveModel(model: TokenReceiveModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveAssetsModel::class) + fun bindsTokenReceiveAssetsModel(model: TokenReceiveAssetsModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveQrCodeModel::class) + fun bindsTokenReceiveQrCodeModel(model: TokenReceiveQrCodeModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveWarningModel::class) + fun bindsTokenReceiveWarningModel(model: TokenReceiveWarningModel): Model +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt new file mode 100644 index 0000000000..960dda1224 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt @@ -0,0 +1,16 @@ +package com.tangem.features.tokenreceive.entity + +import com.tangem.core.ui.extensions.TextReference + +internal data class ReceiveAddress( + val value: String, + val type: Type, +) { + sealed interface Type { + data object Ens : Type + + data class Default( + val displayName: TextReference, + ) : Type + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt new file mode 100644 index 0000000000..bf37b011e3 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt @@ -0,0 +1,59 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +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.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent +import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveAssetsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + analyticsEventHandler: AnalyticsEventHandler, +) : Model() { + + private val params = paramsContainer.require() + + init { + analyticsEventHandler.send( + TokenReceiveNewAnalyticsEvent.ReceiveScreenOpened( + token = params.tokenName, + blockchainName = params.fullName, + ensStatus = configureEnsStatus(), + ), + ) + } + + internal val state: StateFlow + field = MutableStateFlow( + ReceiveAssetsUM( + onCopyClick = params.callback::onCopyClick, + onOpenQrCodeClick = params.callback::onQrCodeClick, + addresses = params.addresses, + showMemoDisclaimer = params.showMemoDisclaimer, + isEnsResultLoading = false, + notificationConfigs = params.notificationConfigs, + fullName = params.fullName, + ), + ) + + private fun configureEnsStatus(): AnalyticsParam.EnsStatus { + val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens } + return if (hasEnsAddress) { + AnalyticsParam.EnsStatus.FULL + } else { + AnalyticsParam.EnsStatus.EMPTY + } + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt new file mode 100644 index 0000000000..183d11d046 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt @@ -0,0 +1,219 @@ +package com.tangem.features.tokenreceive.model + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.push +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 +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.ui.R +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.Asset +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.ens.EnsAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase +import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent +import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class TokenReceiveModel @Inject constructor( + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, + private val getReverseResolvedEnsAddressUseCase: GetReverseResolvedEnsAddressUseCase, + private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), TokenReceiveModelCallback { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + val params = paramsContainer.require() + + val stackNavigation = StackNavigation() + + internal val state: StateFlow + field = MutableStateFlow(getInitState()) + + init { + modelScope.launch { + load() + } + } + + override fun onQrCodeClick(id: Int) { + analyticsEventHandler.send( + TokenReceiveNewAnalyticsEvent.QrScreenOpened( + token = getTokenName(), + blockchainName = params.config.cryptoCurrency.network.name, + ), + ) + stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id)) + } + + override fun onCopyClick(id: Int) { + val addressToCopy = state.value.addresses[id] ?: return + sendCopyActionAnalytic(addressToCopy) + clipboardManager.setText(text = addressToCopy.value, isSensitive = true) + } + + override fun onShareClick(address: String) { + shareManager.shareText(text = address) + } + + override fun onWarningAcknowledged() { + modelScope.launch { + saveViewedTokenReceiveWarningUseCase.invoke( + when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.name + Asset.NFT -> asset.name + }, + ) + stackNavigation.push(configuration = TokenReceiveRoutes.ReceiveAssets) + } + } + + internal fun getTokenName(): String { + return when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.symbol + Asset.NFT -> asset.name + } + } + + private fun mapAddresses(addresses: List): ImmutableMap { + return buildMap { + addresses.mapIndexed { index, model -> + val type = when (model.nameService) { + ReceiveAddressModel.NameService.Default -> { + ReceiveAddress.Type.Default( + displayName = stringReference(model.displayName), + ) + } + ReceiveAddressModel.NameService.Ens -> ReceiveAddress.Type.Ens + } + put( + key = index, + value = ReceiveAddress( + value = model.value, + type = type, + ), + ) + } + }.toPersistentMap() + } + + private suspend fun load() = withContext(dispatchers.default) { + state.value = state.value.copy( + isEnsResultLoading = true, + ) + + val reverseResolveResult = + if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) { + getReverseResolvedEnsAddressUseCase( + userWalletId = params.config.userWalletId, + network = params.config.cryptoCurrency.network, + addresses = params.config.receiveAddress.map { it.value }, + ) + } else { + emptyList() + } + + val currentAddressValues = state.value.addresses.values.map { it.value }.toSet() + + val newEnsAddresses = reverseResolveResult + .filterIsInstance() + .filterNot { it.name in currentAddressValues } + .map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = ReceiveAddress.Type.Ens) } + + val combinedAddresses = (state.value.addresses.values + newEnsAddresses) + .sortedWith(compareByDescending { it.type is ReceiveAddress.Type.Ens }) + + val updatedAddresses = combinedAddresses + .mapIndexed { index, address -> index to address } + .toMap() + .toPersistentMap() + + state.value = state.value.copy( + isEnsResultLoading = false, + addresses = updatedAddresses, + ) + } + + private fun getNotifications(): List { + return buildList { + add( + NotificationUM.Info( + title = resourceReference( + R.string.receive_bottom_sheet_warning_title, + wrappedList( + getTokenName(), + params.config.cryptoCurrency.network.name, + ), + ), + subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description), + iconTint = NotificationConfig.IconTint.Accent, + ), + ) + + params.config.tokenReceiveNotification.map { notification -> + add( + NotificationUM.Warning( + title = resourceReference(notification.title), + subtitle = resourceReference(notification.subtitle), + ), + ) + } + } + } + + private fun getInitState(): TokenReceiveUM { + return TokenReceiveUM( + addresses = mapAddresses(params.config.receiveAddress), + iconState = iconStateConverter.convert(params.config.cryptoCurrency), + network = params.config.cryptoCurrency.network.name, + isEnsResultLoading = false, + notificationConfigs = getNotifications().toImmutableList(), + ) + } + + private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress) { + val event = when (receiveAddress.type) { + is ReceiveAddress.Type.Default -> { + TokenReceiveNewAnalyticsEvent.ButtonCopyAddress( + token = getTokenName(), + blockchainName = params.config.cryptoCurrency.network.name, + ) + } + ReceiveAddress.Type.Ens -> { + TokenReceiveNewAnalyticsEvent.ButtonCopyEns( + token = getTokenName(), + blockchainName = params.config.cryptoCurrency.network.name, + ) + } + } + analyticsEventHandler.send(event) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt new file mode 100644 index 0000000000..68246b663f --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tokenreceive.model + +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.core.ui.extensions.TextReference +import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.ui.state.QrCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveQrCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + QrCodeUM( + network = params.network, + addressValue = params.address.value, + addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY, + onCopyClick = { params.callback.onCopyClick(params.id) }, + onShareClick = params.callback::onShareClick, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt new file mode 100644 index 0000000000..8c686c07a7 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tokenreceive.model + +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.core.ui.extensions.iconResId +import com.tangem.features.tokenreceive.component.TokenReceiveWarningComponent +import com.tangem.features.tokenreceive.ui.state.WarningUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveWarningModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + WarningUM( + iconState = params.iconState, + onWarningAcknowledged = params.callback::onWarningAcknowledged, + network = params.network.name, + networkIcon = params.network.iconResId, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt new file mode 100644 index 0000000000..c98a6e3c12 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt @@ -0,0 +1,19 @@ +package com.tangem.features.tokenreceive.route + +import androidx.compose.runtime.Immutable +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +internal sealed interface TokenReceiveRoutes : Route { + + @Serializable + data object Warning : TokenReceiveRoutes + + @Serializable + data object ReceiveAssets : TokenReceiveRoutes + + @Serializable + data class QrCode(val addressId: Int) : TokenReceiveRoutes +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt new file mode 100644 index 0000000000..94415b927d --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt @@ -0,0 +1,23 @@ +package com.tangem.features.tokenreceive.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SnackbarHostState +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.snackbar.CopiedTextSnackbarHost + +@Composable +internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) { + Box { + content() + CopiedTextSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 80.dp), + ) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt new file mode 100644 index 0000000000..10f41e7e05 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt @@ -0,0 +1,346 @@ +package com.tangem.features.tokenreceive.ui + +import android.content.res.Configuration +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.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.res.getStringSafe +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.buttons.small.TangemIconButton +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.components.notifications.Notification +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.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.launch + +@Composable +internal fun TokenReceiveAssetsContent(assetsUM: ReceiveAssetsUM) { + val snackbarHostState = remember(::SnackbarHostState) + + ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 8.dp, + ), + ) { + SpacerH8() + + Info( + showMemoDisclaimer = assetsUM.showMemoDisclaimer, + notificationConfigs = assetsUM.notificationConfigs, + ) + + SpacerH12() + + AddressBlock( + onCopyClick = assetsUM.onCopyClick, + onOpenQrCodeClick = assetsUM.onOpenQrCodeClick, + addresses = assetsUM.addresses, + snackbarHostState = snackbarHostState, + fullName = assetsUM.fullName, + ) + + if (assetsUM.isEnsResultLoading) { + LoadingBlock() + } + } + } +} + +@Composable +private fun Info( + notificationConfigs: ImmutableList, + showMemoDisclaimer: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + ) { + if (showMemoDisclaimer) { + Text( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } + + notificationConfigs.fastForEach { + key(it.hashCode()) { + Notification(config = it.config) + } + } + } +} + +@Composable +private fun AddressBlock( + onOpenQrCodeClick: (id: Int) -> Unit, + onCopyClick: (id: Int) -> Unit, + addresses: ImmutableMap, + snackbarHostState: SnackbarHostState, + fullName: String, +) { + val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = context.resources + + addresses.entries.toList().fastForEach { entry -> + when (entry.value.type) { + is ReceiveAddress.Type.Default -> { + key(entry.key) { + AddressItem( + onCopyClick = { + onCopyClick(entry.key) + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getStringSafe( + R.string.wallet_notification_address_copied, + ), + ) + } + }, + onOpenQrCodeClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onOpenQrCodeClick(entry.key) + }, + fullName = fullName, + address = entry.value.value, + ) + SpacerH8() + } + } + ReceiveAddress.Type.Ens -> { + key(entry.key) { + EnsItem( + onCopyClick = { + onCopyClick(entry.key) + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getStringSafe( + R.string.wallet_notification_address_copied, + ), + ) + } + }, + address = entry.value.value, + ) + SpacerH8() + } + } + } + } +} + +@Composable +private fun AddressItem( + onOpenQrCodeClick: () -> Unit, + fullName: String, + onCopyClick: () -> Unit, + address: String, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action), + onClick = onOpenQrCodeClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IdentIcon( + address = address, + modifier = Modifier + .size(size = 36.dp) + .clip(shape = RoundedCornerShape(18.dp)), + ) + + SpacerW12() + + Column(modifier = Modifier.weight(1f)) { + EllipsisText( + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, fullName), + ellipsis = TextEllipsis.Middle, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + + EllipsisText( + text = address, + ellipsis = TextEllipsis.Middle, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } + + SpacerW12() + + TangemIconButton( + modifier = Modifier.size(TangemTheme.dimens.size28), + iconRes = R.drawable.ic_qrcode_new_24, + onClick = onOpenQrCodeClick, + ) + + SpacerW8() + + TangemIconButton( + modifier = Modifier.size(TangemTheme.dimens.size28), + iconRes = R.drawable.ic_copy_new_24, + onClick = onCopyClick, + ) + } + } +} + +@Composable +private fun EnsItem(onCopyClick: () -> Unit, address: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier.size(36.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_ens_36), + contentDescription = null, + ) + + SpacerW12() + + EllipsisText( + modifier = Modifier.weight(1f), + text = address, + ellipsis = TextEllipsis.Middle, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + + TangemIconButton( + modifier = Modifier.size(28.dp), + iconRes = R.drawable.ic_copy_new_24, + onClick = onCopyClick, + ) + } + } +} + +@Composable +private fun LoadingBlock(modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action), + ) { + Box( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 64.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(TangemTheme.dimens.spacing8), + ) + } + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TokenReceiveAssetsContent( + @PreviewParameter(TokenReceiveAssetsContentProvider::class) params: ReceiveAssetsUM, +) { + TangemThemePreview { + TokenReceiveAssetsContent(assetsUM = params) + } +} + +private class TokenReceiveAssetsContentProvider : PreviewParameterProvider { + val address = ReceiveAddress( + value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d", + type = ReceiveAddress.Type.Default( + displayName = stringReference("Etherium address"), + ), + ) + private val config = ReceiveAssetsUM( + notificationConfigs = + persistentListOf( + NotificationUM.Warning( + title = stringReference("Send only XLM on the Ethereum network"), + subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description), + ), + ), + addresses = persistentMapOf( + 0 to address, + 1 to address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"), + ), + showMemoDisclaimer = false, + onCopyClick = {}, + onOpenQrCodeClick = {}, + isEnsResultLoading = true, + fullName = "Etherium", + ) + + override val values: Sequence + get() = sequenceOf(config) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt new file mode 100644 index 0000000000..75ee1fb93f --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt @@ -0,0 +1,93 @@ +package com.tangem.features.tokenreceive.ui + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.fillMaxSize +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.fade +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.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.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes + +@Composable +internal fun TokenReceiveContentSheet( + route: TokenReceiveRoutes, + onCloseClick: () -> Unit, + onBackClick: () -> Unit, + contentStack: ChildStack, +) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onCloseClick, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = onBackClick, + containerColor = TangemTheme.colors.background.tertiary, + title = { + Title( + route = route, + onBackClick = onBackClick, + onCloseClick = onCloseClick, + ) + }, + content = { + TokenReceiveContent( + stackState = contentStack, + modifier = Modifier, + ) + }, + ) +} + +@Composable +private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { + when (route) { + is TokenReceiveRoutes.QrCode -> { + TangemModalBottomSheetTitle( + startIconRes = R.drawable.ic_back_24, + onStartClick = onBackClick, + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.ReceiveAssets -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.domain_receive_assets_navigation_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.Warning -> { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + } +} + +@Composable +internal fun TokenReceiveContent( + stackState: ChildStack, + modifier: Modifier = Modifier, +) { + Children( + stack = stackState, + animation = stackAnimation(fade()), + modifier = modifier + .fillMaxSize() + .animateContentSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt new file mode 100644 index 0000000000..d248c1b34d --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt @@ -0,0 +1,195 @@ +package com.tangem.features.tokenreceive.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.res.getStringSafe +import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tokenreceive.impl.R +import com.tangem.features.tokenreceive.ui.state.QrCodeUM +import kotlinx.coroutines.launch + +@Composable +internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) { + val snackbarHostState = remember(::SnackbarHostState) + val qrCodePainter = rememberQrPainter(content = qrCodeUM.addressValue) + + ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.tertiary) + .padding(bottom = 16.dp) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH8() + + QrCodePage( + addressFullName = qrCodeUM.addressName, + addressValue = qrCodeUM.addressValue, + network = qrCodeUM.network, + qrCodePainter = qrCodePainter, + ) + SpacerH24() + + Buttons( + onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) }, + onCopyClick = qrCodeUM.onCopyClick, + snackbarHostState = snackbarHostState, + ) + } + } +} + +@Composable +private fun QrCodePage(addressFullName: TextReference, addressValue: String, network: String, qrCodePainter: Painter) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36)) { + Text( + text = stringResourceSafe( + R.string.receive_bottom_sheet_warning_message_compact, + addressFullName.resolveReference(), + network, + ), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + + SpacerH(20.dp) + + Image( + painter = qrCodePainter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(248.dp), + ) + + SpacerH24() + } + Text( + text = stringResourceSafe(R.string.wc_common_address), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle2, + ) + + SpacerH2() + + Text( + text = addressValue, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle1, + ) + } +} + +@Composable +private fun Buttons( + snackbarHostState: SnackbarHostState, + onShareClick: () -> Unit, + onCopyClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = context.resources + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + SecondaryButtonIconStart( + modifier = Modifier.weight(1f), + text = stringResourceSafe(id = R.string.common_copy), + iconResId = R.drawable.ic_copy_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onCopyClick() + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getStringSafe(R.string.wallet_notification_address_copied), + ) + } + }, + ) + + SecondaryButtonIconStart( + modifier = Modifier.weight(1f), + text = stringResourceSafe(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onShareClick() + }, + ) + } +} + +@Composable +private fun rememberQrPainter(content: String, size: Dp = 248.dp, padding: Dp = 0.dp): BitmapPainter { + val density = LocalDensity.current + return remember(content) { + BitmapPainter( + content.toQrCode( + sizePx = with(density) { size.roundToPx() }, + paddingPx = with(density) { padding.roundToPx() }, + ).asImageBitmap(), + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TokenReceiveQrCodeContent( + @PreviewParameter(TokenReceiveQrCodeContentPreviewProvider::class) qrCodeUM: QrCodeUM, +) { + TangemThemePreview { + TokenReceiveQrCodeContent(qrCodeUM = qrCodeUM) + } +} + +private class TokenReceiveQrCodeContentPreviewProvider : PreviewParameterProvider { + private val config = QrCodeUM( + network = "Ethereum", + addressName = stringReference("Etherium"), + addressValue = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d", + onCopyClick = {}, + onShareClick = {}, + ) + + override val values: Sequence + get() = sequenceOf(config) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt new file mode 100644 index 0000000000..2aa8aeff9c --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt @@ -0,0 +1,142 @@ +package com.tangem.features.tokenreceive.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.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tokenreceive.impl.R +import com.tangem.features.tokenreceive.ui.state.WarningUM + +@Composable +internal fun TokenReceiveWarningContent(warningUM: WarningUM) { + val hapticFeedback = LocalHapticFeedback.current + + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CurrencyIcon( + modifier = Modifier.size(size = 56.dp), + state = warningUM.iconState, + shouldDisplayNetwork = false, + iconSize = 56.dp, + ) + + SpacerH24() + + WarningBlock(networkIcon = warningUM.networkIcon, networkName = warningUM.network) + + SpacerH12() + + Text( + textAlign = TextAlign.Center, + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + + SpacerH(48.dp) + + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.common_got_it), + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + warningUM.onWarningAcknowledged() + }, + ) + } +} + +@Composable +fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + textAlign = TextAlign.Center, + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = networkIcon), + tint = Color.Unspecified, + contentDescription = null, + ) + + Text( + textAlign = TextAlign.Center, + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TokenReceiveWarningContent( + @PreviewParameter(TokenReceiveWarningContentProvider::class) warningUM: WarningUM, +) { + TangemThemePreview { + TokenReceiveWarningContent(warningUM = warningUM) + } +} + +private class TokenReceiveWarningContentProvider : PreviewParameterProvider { + val iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = null, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + showCustomBadge = false, + ) + + override val values: Sequence + get() = sequenceOf( + WarningUM( + iconState = iconState, + onWarningAcknowledged = {}, + network = "Etherium", + networkIcon = R.drawable.ic_eth_16, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt new file mode 100644 index 0000000000..f9425a4fc6 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.core.ui.extensions.TextReference + +internal data class QrCodeUM( + val onCopyClick: () -> Unit, + val onShareClick: (String) -> Unit, + val addressName: TextReference, + val addressValue: String, + val network: String, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt new file mode 100644 index 0000000000..3953389152 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal data class ReceiveAssetsUM( + val showMemoDisclaimer: Boolean, + val addresses: ImmutableMap, + val onOpenQrCodeClick: (id: Int) -> Unit, + val onCopyClick: (id: Int) -> Unit, + val isEnsResultLoading: Boolean, + val notificationConfigs: ImmutableList, + val fullName: String, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt new file mode 100644 index 0000000000..27eecde6a3 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal data class TokenReceiveUM( + val network: String, + val iconState: CurrencyIconState, + val addresses: ImmutableMap, + val isEnsResultLoading: Boolean, + val notificationConfigs: ImmutableList, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt new file mode 100644 index 0000000000..c31262fd58 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState + +internal data class WarningUM( + val network: String, + val networkIcon: Int, + val iconState: CurrencyIconState, + val onWarningAcknowledged: () -> Unit, +) \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 4e929dfdb3..5da95d55a3 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) @@ -102,5 +101,8 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.txhistory.api) implementation(projects.features.sendV2.api) + implementation(projects.features.tokenRecieve.api) + + implementation(deps.decompose.ext.compose) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 410b089abe..2291d82965 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -4,16 +4,24 @@ 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.router.slot.childSlot +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe 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.ui.components.NavigationBar3ButtonsScrim +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.TokenDetailsComponent +import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.txhistory.component.TxHistoryComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -25,6 +33,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Assisted params: TokenDetailsComponent.Params, tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory, txHistoryComponentFactory: TxHistoryComponent.Factory, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : TokenDetailsComponent, AppComponentContext by appComponentContext { private val model: TokenDetailsModel = getOrCreateModel(params) @@ -37,6 +46,13 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( ), ) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + init { lifecycle.subscribe( onPause = model::onPause, @@ -54,12 +70,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NavigationBar3ButtonsScrim() TokenDetailsScreen( state = state, tokenMarketBlockComponent = tokenMarketBlockComponent, txHistoryComponent = txHistoryComponent, ) + bottomSheet.child?.instance?.BottomSheet() } private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? { @@ -68,6 +86,17 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( return TokenMarketBlockComponent.Params(cryptoCurrency = this) } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + @AssistedFactory interface Factory : TokenDetailsComponent.Factory { override fun create( 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..7f2de4ef2c 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 @@ -4,6 +4,8 @@ import androidx.compose.runtime.Stable import androidx.paging.cachedIn import arrow.core.getOrElse import arrow.core.merge +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -33,23 +35,26 @@ 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.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig 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.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 @@ -60,16 +65,12 @@ import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.error.IncompleteTransactionError import com.tangem.domain.transaction.error.OpenTrustlineError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.AssociateAssetUseCase -import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase -import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase -import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase +import com.tangem.domain.transaction.usecase.* 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 @@ -83,6 +84,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.e import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -133,12 +135,14 @@ 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, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val sendFeatureToggles: SendFeatureToggles, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model(), TokenDetailsClickIntents { private val params = paramsContainer.require() @@ -161,6 +165,8 @@ internal class TokenDetailsModel @Inject constructor( /** Transaction id to check for status */ private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), @@ -169,7 +175,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, ) @@ -597,20 +602,27 @@ internal class TokenDetailsModel @Inject constructor( } modelScope.launch { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) - - internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( - currency = cryptoCurrency, - networkAddress = networkAddress, - onCopyClick = { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol)) - shareManager.shareText(text = it) - }, - ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + bottomSheetNavigation.activate( + configuration = configureReceiveAddresses(addresses = networkAddress), + ) + } else { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) + internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( + currency = cryptoCurrency, + networkAddress = networkAddress, + onCopyClick = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + analyticsEventsHandler.send( + TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol), + ) + shareManager.shareText(text = it) + }, + ) + } } } @@ -1064,6 +1076,43 @@ internal class TokenDetailsModel @Inject constructor( .launchIn(modelScope) } + private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenReceiveConfig { + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L } 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 Unit, ) : WalletSettingsItemUM() - data class WithText( + data class CardBlock( override val id: String, val title: TextReference, val text: TextReference, val isEnabled: Boolean, + val imageState: ImageState, val onClick: () -> Unit, ) : WalletSettingsItemUM() @@ -43,4 +46,44 @@ internal sealed class WalletSettingsItemUM { val title: TextReference, val description: TextReference, ) : WalletSettingsItemUM() + + data class UpgradeWallet( + override val id: String, + val title: TextReference, + val description: TextReference, + val onClick: () -> Unit, + val onDismissClick: () -> Unit, + ) : WalletSettingsItemUM() +} + +@Immutable +internal sealed class WalletSettingsAccountsUM : WalletSettingsItemUM() { + + data class Header( + override val id: String, + val text: TextReference, + ) : WalletSettingsAccountsUM() + + data class Account( + override val id: String, + val accountName: TextReference, + val accountIconUM: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val networksInfo: TextReference, + val onClick: () -> Unit, + ) : WalletSettingsAccountsUM() + + data class Footer( + override val id: String, + val addAccount: AddAccountUM, + val archivedAccounts: BlockUM, + val description: TextReference, + ) : WalletSettingsAccountsUM() { + + data class AddAccountUM( + val title: TextReference, + val addAccountEnabled: Boolean, + val onAddAccountClick: () -> Unit, + ) + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt index 3cadad2268..04014e4662 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -9,4 +9,6 @@ internal data class WalletSettingsUM( val items: PersistentList, val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, + val isWalletBackedUp: Boolean = true, + val walletUpgradeDismissed: Boolean = false, ) \ 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..66ddf7f18f 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 @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.model import android.os.Build +import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -15,14 +16,17 @@ 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.* 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 import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.DisableWalletNFTUseCase import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase @@ -30,9 +34,6 @@ import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUse 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 @@ -42,8 +43,9 @@ import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotification 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.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.ItemsBuilder -import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList @@ -53,7 +55,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class WalletSettingsModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, @@ -62,10 +64,11 @@ internal class WalletSettingsModel @Inject constructor( private val messageSender: UiMessageSender, private val deleteWalletUseCase: DeleteWalletUseCase, private val itemsBuilder: ItemsBuilder, + private val accountItemsDelegate: AccountItemsDelegate, override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, - private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, + walletCardItemDelegateFactory: WalletCardItemDelegate.Factory, private val isDemoCardUseCase: IsDemoCardUseCase, getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, private val enableWalletNFTUseCase: EnableWalletNFTUseCase, @@ -75,14 +78,16 @@ 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, + private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase, + private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() val dialogNavigation = SlotNavigation() val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val walletCardItemDelegate = walletCardItemDelegateFactory.create(dialogNavigation) val state: MutableStateFlow = MutableStateFlow( value = WalletSettingsUM( @@ -90,34 +95,62 @@ internal class WalletSettingsModel @Inject constructor( items = persistentListOf(), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, + isWalletBackedUp = true, + walletUpgradeDismissed = 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 { - combine( - getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), + fun combineUI(wallet: UserWallet) = combine( getWalletNFTEnabledUseCase.invoke(params.userWalletId), getWalletNotificationsEnabledUseCase(params.userWalletId), - ) { maybeWallet, nftEnabled, notificationsEnabled -> - val wallet = maybeWallet.getOrNull() ?: return@combine - val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + isUpgradeWalletNotificationEnabledUseCase(params.userWalletId), + walletCardItemDelegate.cardItemFlow(wallet), + ) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem -> + val isWalletBackedUp = when (wallet) { + is UserWallet.Hot -> wallet.backedUp + is UserWallet.Cold -> true + } val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && !getIsHuaweiDeviceWithoutGoogleServicesUseCase() state.update { value -> value.copy( items = buildItems( userWallet = wallet, - dialogNavigation = dialogNavigation, - isRenameWalletAvailable = isRenameWalletAvailable, + cardItem = cardItem, isNFTEnabled = nftEnabled, isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), - isHotWalletEnabled = hotWalletFeatureToggles.isHotWalletEnabled, + isUpgradeNotificationEnabled = isUpgradeNotificationEnabled, ), + isWalletBackedUp = isWalletBackedUp, ) } } + getWalletUseCase.invokeFlow(params.userWalletId) + .distinctUntilChanged() + .filterIsInstance>() + .flatMapLatest { combineUI(it.value) } .launchIn(modelScope) } @@ -133,60 +166,65 @@ internal class WalletSettingsModel @Inject constructor( private fun buildItems( userWallet: UserWallet, - dialogNavigation: SlotNavigation, - isRenameWalletAvailable: Boolean, + cardItem: WalletSettingsItemUM.CardBlock, isNFTEnabled: Boolean, 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() }, - ) + isUpgradeNotificationEnabled: Boolean, + ): PersistentList { + val isMultiCurrency = when (userWallet) { + is UserWallet.Cold -> userWallet.isMultiCurrency + is UserWallet.Hot -> true + } + return itemsBuilder.buildItems( + userWallet = userWallet, + cardItem = cardItem, + 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, + 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, - ) - - private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { - val config = DialogConfig.RenameWallet( - userWalletId = userWallet.walletId, - currentName = userWallet.name, + 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, + walletUpgradeDismissed = isUpgradeNotificationEnabled, + onUpgradeWalletClick = ::onUpgradeWalletClick, + onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick, + accountsUM = with(accountItemsDelegate) { listOf() }, // todo account ) - - dialogNavigation.activate(config) } private fun forgetWallet() = modelScope.launch { @@ -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,22 @@ 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)) + } + } + + private fun onUpgradeWalletClick() { + // TODO [REDACTED_TASK_KEY] + } + + private fun onDismissUpgradeWalletClick() { + modelScope.launch { + dismissUpgradeWalletNotificationUseCase.invoke(params.userWalletId) + } + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index fe95f8129a..c0d5e0cd86 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -2,39 +2,52 @@ package com.tangem.feature.walletsettings.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.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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.platform.testTag +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountRow +import com.tangem.common.ui.userwallet.CardImage +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockItem +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig 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.core.ui.test.WalletSettingsScreenTestTags -import com.tangem.core.ui.utils.requestPermission import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @Composable internal fun WalletSettingsScreen( @@ -70,7 +83,6 @@ internal fun WalletSettingsScreen( private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { LazyColumn( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), contentPadding = PaddingValues( top = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16, @@ -88,8 +100,17 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { items = state.items, key = WalletSettingsItemUM::id, ) { item -> - val itemModifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) + val offsetModifier = when (item) { + is WalletSettingsAccountsUM.Account, + is WalletSettingsAccountsUM.Footer, + -> Modifier.padding(horizontal = TangemTheme.dimens.spacing16) + else -> Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ) + } + val itemModifier = offsetModifier .fillMaxWidth() .testTag(WalletSettingsScreenTestTags.SCREEN_ITEM) @@ -98,7 +119,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) - is WalletSettingsItemUM.WithText -> TextBlock( + is WalletSettingsItemUM.CardBlock -> CardBlock( modifier = itemModifier, model = item, ) @@ -116,21 +137,16 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) + is WalletSettingsItemUM.UpgradeWallet -> UpgradeWalletBlock( + modifier = itemModifier, + model = item, + ) + is WalletSettingsAccountsUM.Header -> AccountsHeader(item, itemModifier) + is WalletSettingsAccountsUM.Account -> AccountItem(item, itemModifier) + is WalletSettingsAccountsUM.Footer -> AccountsFooter(item, itemModifier) } } } - - val requestPushPermission = requestPermission( - onAllow = { state.onPushNotificationPermissionGranted(true) }, - onDeny = { state.onPushNotificationPermissionGranted(false) }, - permission = PUSH_PERMISSION, - ) - - if (state.requestPushNotificationsPermission) { - LaunchedEffect(Unit) { - requestPushPermission() - } - } } @Composable @@ -168,30 +184,40 @@ private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier } @Composable -private fun TextBlock(model: WalletSettingsItemUM.WithText, modifier: Modifier = Modifier) { +private fun CardBlock(model: WalletSettingsItemUM.CardBlock, modifier: Modifier = Modifier) { BlockCard( modifier = modifier.fillMaxWidth(), enabled = model.isEnabled, onClick = model.onClick, ) { - Column( + Row( modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = model.title.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - Text( - text = model.text.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - overflow = TextOverflow.Ellipsis, + CardImage(model.imageState) + Column(modifier = Modifier.weight(1f)) { + Text( + text = model.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = model.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + SecondarySmallButton( + config = SmallButtonConfig( + enabled = model.isEnabled, + text = resourceReference(R.string.common_rename), + onClick = model.onClick, + ), ) } } @@ -225,6 +251,21 @@ private fun SwitchBlock(model: WalletSettingsItemUM.WithSwitch, modifier: Modifi } } +@Composable +private fun UpgradeWalletBlock(model: WalletSettingsItemUM.UpgradeWallet, modifier: Modifier = Modifier) { + Notification( + config = NotificationConfig( + title = model.title, + subtitle = model.description, + iconResId = R.drawable.ic_hardware_backup_36, + iconSize = 36.dp, + onClick = model.onClick, + onCloseClick = model.onDismissClick, + ), + modifier = modifier, + ) +} + @Composable private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermission, modifier: Modifier = Modifier) { Notification( @@ -237,6 +278,134 @@ private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermi ) } +@Composable +private fun AccountsHeader(model: WalletSettingsAccountsUM.Header, modifier: Modifier = Modifier) { + Text( + modifier = modifier + .background( + shape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ), + color = TangemTheme.colors.background.primary, + ) + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, + ), + text = model.text.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun AccountItem(model: WalletSettingsAccountsUM.Account, modifier: Modifier = Modifier) { + val subtitle = stringResourceSafe( + id = R.string.account_label_tokens_info, + formatArgs = arrayOf( + model.tokensInfo.resolveReference(), + model.networksInfo.resolveReference(), + ), + ) + AccountRow( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .clickable(onClick = model.onClick) + .padding(12.dp), + title = model.accountName, + subtitle = stringReference(subtitle), + icon = model.accountIconUM, + ) +} + +@Composable +private fun AccountsFooter(model: WalletSettingsAccountsUM.Footer, modifier: Modifier = Modifier) { + Column(modifier) { + Column( + modifier = Modifier.background( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ), + color = TangemTheme.colors.background.primary, + ), + ) { + AddAccountRow(model.addAccount) + SpacerH( + height = TangemTheme.dimens.size0_5, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary), + ) + BlockItem( + modifier = Modifier.fillMaxWidth(), + model = model.archivedAccounts, + ) + } + SpacerH8() + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = model.description.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun AddAccountRow(model: WalletSettingsAccountsUM.Footer.AddAccountUM, modifier: Modifier = Modifier) { + val iconTint: Color + val backgroundColor: Color + val textColor: Color + if (model.addAccountEnabled) { + iconTint = TangemTheme.colors.icon.accent + backgroundColor = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + textColor = TangemTheme.colors.text.accent + } else { + iconTint = TangemTheme.colors.icon.inactive + backgroundColor = TangemTheme.colors.field.primary + textColor = TangemTheme.colors.text.disabled + } + Row( + modifier = modifier + .clickable(onClick = model.onAddAccountClick) + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(backgroundColor) + .clickable(onClick = { model.onAddAccountClick() }), + ) { + Icon( + modifier = Modifier.size(18.dp), + tint = iconTint, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_24), + contentDescription = null, + ) + } + + Text( + text = model.title.resolveReference(), + color = textColor, + style = TangemTheme.typography.subtitle1, + ) + } +} + @Composable private fun DescriptionWithMoreBlock( model: WalletSettingsItemUM.DescriptionWithMore, @@ -266,4 +435,21 @@ private fun Preview_WalletSettingsScreen() { PreviewWalletSettingsComponent().Content(modifier = Modifier.fillMaxSize()) } } + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_WalletSettingsScreen1() { + TangemThemePreview { + UpgradeWalletBlock( + model = WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = stringReference("Upgrade wallet with a hardware backup"), + description = stringReference("Keep your crypto safe with Tangem’s best-in-class hardware wallet."), + onClick = {}, + onDismissClick = {}, + ), + ) + } +} // endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt new file mode 100644 index 0000000000..7e9939d2e3 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -0,0 +1,111 @@ +package com.tangem.feature.walletsettings.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.account.toUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.pluralReference +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.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM +import com.tangem.feature.walletsettings.impl.R +import javax.inject.Inject + +@ModelScoped +internal class AccountItemsDelegate @Inject constructor( + private val router: Router, + private val messageSender: UiMessageSender, +) { + + fun buildUiList(userWalletId: UserWalletId, accounts: List): List = buildList { + WalletSettingsAccountsUM.Header( + id = "accounts_header", + text = resourceReference(R.string.common_accounts), + ).let(::add) + + addAll(accounts.map(::mapAccount)) + + val addAccountEnabled = true // todo account + WalletSettingsAccountsUM.Footer( + id = "accounts_footer", + addAccount = AddAccountUM( + title = resourceReference(R.string.account_form_title_create), + addAccountEnabled = addAccountEnabled, + onAddAccountClick = { + if (addAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog() + }, + ), + archivedAccounts = BlockUM( + text = resourceReference(R.string.account_archived_accounts), + iconRes = R.drawable.ic_archive_24, + onClick = { openArchivedAccounts(userWalletId) }, + ), + description = resourceReference(R.string.account_reorder_description), + ).let(::add) + } + + private fun mapAccount(account: Account): WalletSettingsAccountsUM = when (account) { + is Account.CryptoPortfolio -> account.mapCryptoPortfolio() + } + + private fun Account.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM { + return WalletSettingsAccountsUM.Account( + id = accountId.value, + accountName = stringReference(accountName.value), + accountIconUM = icon.toUM(), + tokensInfo = pluralReference( + R.plurals.common_tokens_count, + count = tokensCount, + formatArgs = wrappedList(tokensCount), + ), + networksInfo = pluralReference( + R.plurals.common_networks_count, + count = networksCount, + formatArgs = wrappedList(networksCount), + ), + onClick = { openAccountDetails(this) }, + ) + } + + private fun openAccountDetails(account: Account) { + router.push(AppRoute.AccountDetails(account)) + } + + private fun openArchivedAccounts(userWalletId: UserWalletId) { + router.push(AppRoute.ArchivedAccountList(userWalletId)) + } + + private fun openAddAccount(userWalletId: UserWalletId) { + router.push(AppRoute.CreateAccount(userWalletId)) + } + + private fun canNotAddAccountDialog() { + val firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = { }, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_add_limit_dialog_title), + message = resourceReference( + id = R.string.account_add_limit_dialog_description, + formatArgs = wrappedList(MAX_ACCOUNT_COUNT.toString()), + ), + firstActionBuilder = { firstAction }, + ), + ) + } + + companion object { + // todo account use domain const? + private const val MAX_ACCOUNT_COUNT = 20 + } +} \ 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..6d2bf36754 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.WalletSettingsAccountsUM 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,12 +28,12 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") fun buildItems( - userWalletId: UserWalletId, - userWalletName: String, + userWallet: UserWallet, + cardItem: WalletSettingsItemUM.CardBlock, + accountsUM: List, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, - isRenameWalletAvailable: Boolean, isNFTFeatureEnabled: Boolean, isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit, @@ -40,28 +43,32 @@ internal class ItemsBuilder @Inject constructor( onCheckedNotificationsChanged: (Boolean) -> Unit, onNotificationsDescriptionClick: () -> Unit, forgetWallet: () -> Unit, - renameWallet: () -> Unit, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, - isHotWalletEnabled: Boolean, + onAccessCodeClick: () -> Unit, + walletUpgradeDismissed: Boolean, + onUpgradeWalletClick: () -> Unit, + onDismissUpgradeWalletClick: () -> Unit, ): PersistentList = persistentListOf() - .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) - .run { - if (isNFTFeatureEnabled) { - add(buildNFTItem(isNFTEnabled, onCheckedNFTChange)) - } else { - this - } - } + .add(cardItem) + .addAll( + buildUpgradeWalletItem( + userWallet = userWallet, + walletUpgradeDismissed = walletUpgradeDismissed, + onUpgradeWalletClick = onUpgradeWalletClick, + onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, + ), + ) + .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) + .addAll(accountsUM) .add( buildCardItem( - userWalletId = userWalletId, + userWallet = userWallet, isLinkMoreCardsAvailable = isLinkMoreCardsAvailable, isReferralAvailable = isReferralAvailable, isManageTokensAvailable = isManageTokensAvailable, onLinkMoreCardsClick = onLinkMoreCardsClick, onReferralClick = onReferralClick, - isHotWalletEnabled = isHotWalletEnabled, ), ) .addAll( @@ -73,8 +80,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, @@ -92,15 +118,6 @@ internal class ItemsBuilder @Inject constructor( } } - private fun buildNameItem(walletName: String, isRenameWalletAvailable: Boolean, renameWallet: () -> Unit) = - WalletSettingsItemUM.WithText( - id = "wallet_name", - title = resourceReference(id = R.string.settings_wallet_name_title), - text = stringReference(walletName), - isEnabled = isRenameWalletAvailable, - onClick = renameWallet, - ) - private fun buildNFTItem(isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit) = WalletSettingsItemUM.WithSwitch( id = "nft", @@ -109,6 +126,28 @@ internal class ItemsBuilder @Inject constructor( onCheckedChange = onCheckedNFTChange, ) + private fun buildUpgradeWalletItem( + userWallet: UserWallet, + walletUpgradeDismissed: Boolean, + onUpgradeWalletClick: () -> Unit, + onDismissUpgradeWalletClick: () -> Unit, + ): List = when (userWallet) { + is UserWallet.Cold -> emptyList() + is UserWallet.Hot -> if (!walletUpgradeDismissed) { + listOf( + WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = resourceReference(id = R.string.hw_upgrade_to_cold_banner_title), + description = resourceReference(id = R.string.hw_upgrade_to_cold_banner_description), + onClick = onUpgradeWalletClick, + onDismissClick = onDismissUpgradeWalletClick, + ), + ) + } else { + emptyList() + } + } + private fun buildNotificationsPermissionItem() = WalletSettingsItemUM.NotificationPermission( id = "notifications_permission", title = resourceReference(id = R.string.transaction_notifications_warning_title), @@ -133,17 +172,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 +220,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 +250,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-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt new file mode 100644 index 0000000000..322eaaac62 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.walletsettings.utils + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.features.wallet.utils.UserWalletImageFetcher +import com.tangem.operations.attestation.ArtworkSize +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow + +internal class WalletCardItemDelegate @AssistedInject constructor( + private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, + private val walletImageFetcher: UserWalletImageFetcher, + @Assisted private val dialogNavigation: SlotNavigation, +) { + + fun cardItemFlow(wallet: UserWallet): Flow = combine( + flow = walletImageFetcher.walletImage(wallet, ArtworkSize.SMALL), + flow2 = flow { emit(getShouldSaveUserWalletsSyncUseCase()) }, + transform = { imageState, isRenameAvailable -> + val walletName = wallet.name + WalletSettingsItemUM.CardBlock( + id = "wallet_name", + title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder), + text = stringReference(walletName), + isEnabled = isRenameAvailable, + onClick = { openRenameWalletDialog(wallet) }, + imageState = imageState, + ) + }, + ) + + private fun openRenameWalletDialog(userWallet: UserWallet) { + val config = DialogConfig.RenameWallet( + userWalletId = userWallet.walletId, + currentName = userWallet.name, + ) + dialogNavigation.activate(config) + } + + @AssistedFactory + interface Factory { + fun create(dialogNavigation: SlotNavigation): WalletCardItemDelegate + } +} \ No newline at end of file diff --git a/features/wallet/api/build.gradle.kts b/features/wallet/api/build.gradle.kts index 3b1f5eeea2..4c23e065f7 100644 --- a/features/wallet/api/build.gradle.kts +++ b/features/wallet/api/build.gradle.kts @@ -15,6 +15,9 @@ dependencies { /** Project - Domain */ implementation(projects.domain.models) + /** Tangem libraries */ + implementation(tangemDeps.card.core) + /** Core */ implementation(projects.core.ui) implementation(projects.core.decompose) diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt new file mode 100644 index 0000000000..42738f4e11 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt @@ -0,0 +1,20 @@ +package com.tangem.features.wallet.utils + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.operations.attestation.ArtworkSize +import kotlinx.coroutines.flow.Flow + +interface UserWalletImageFetcher { + + fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow + fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow + fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow + + fun walletsImage( + wallets: Collection, + size: ArtworkSize, + ): Flow> +} \ 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 646bb012f2..1df9de0f95 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) @@ -97,6 +96,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.notifications) implementation(projects.domain.notifications.toggles) + implementation(projects.domain.transaction) /** Feature Apis */ implementation(projects.features.details.api) @@ -114,6 +114,8 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.kyc.api) + implementation(projects.features.tokenRecieve.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/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 8ee7b2c6c2..8b75c0188c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -24,11 +24,13 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +@Suppress("LongParameterList") internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, @@ -36,6 +38,7 @@ internal class WalletComponent @AssistedInject constructor( private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() @@ -78,6 +81,15 @@ internal class WalletComponent @AssistedInject constructor( source = AppRoute.PushNotification.Source.Main, ), ) + is WalletDialogConfig.TokenReceive -> { + tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = dialogConfig.tokenReceiveConfig, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } } }, ) 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..71d5e76c5e 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 @@ -29,27 +29,31 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig 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 import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase 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.transaction.usecase.GetEnsNameUseCase 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 @@ -61,6 +65,7 @@ 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.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -128,6 +133,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val shareManager: ShareManager, private val appRouter: AppRouter, private val rampStateManager: RampStateManager, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -172,13 +180,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event?.let { analyticsEventHandler.send(it) } - stateHolder.showBottomSheet( - createReceiveBottomSheetContent( - currency = cryptoCurrencyStatus.currency, - addresses = cryptoCurrencyStatus.value.networkAddress ?: return, - ), - userWalletId, - ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + stateHolder.hideBottomSheet() + modelScope.launch { + configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let { + router.openTokenReceiveBottomSheet(it) + } + } + } else { + stateHolder.showBottomSheet( + createReceiveBottomSheetContent( + currency = cryptoCurrencyStatus.currency, + addresses = cryptoCurrencyStatus.value.networkAddress ?: return, + ), + userWalletId, + ) + } } override fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? { @@ -639,4 +656,45 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( targetRoute } } + + private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? { + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null + val userWalletId = stateHolder.getSelectedWalletId() + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + address = networkAddress.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + networkAddress.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file 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/di/WalletFeatureModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt index 74a02c34b7..1a925f467e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt @@ -4,8 +4,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.feature.wallet.DefaultWalletEntryComponent import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel import com.tangem.feature.wallet.child.wallet.model.WalletModel +import com.tangem.feature.wallet.utils.DefaultUserWalletImageFetcher import com.tangem.feature.wallet.utils.DefaultUserWalletsFetcher import com.tangem.features.wallet.WalletEntryComponent +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.features.wallet.utils.UserWalletsFetcher import dagger.Binds import dagger.Module @@ -13,6 +15,7 @@ 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) @@ -24,6 +27,10 @@ internal interface WalletFeatureModule { @Binds fun bindUserWalletsFetcher(impl: DefaultUserWalletsFetcher.Factory): UserWalletsFetcher.Factory + @Binds + @Singleton + fun bindUserWalletImageFetcher(impl: DefaultUserWalletImageFetcher): UserWalletImageFetcher + @Binds @IntoMap @ClassKey(WalletModel::class) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index d57ed3079f..9e485dad2e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM 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.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo @@ -117,7 +120,13 @@ internal object WalletScreenPreviewData { buttons = persistentListOf(buyButton), warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, - WalletNotification.FinishWalletActivation { }, + WalletNotification.FinishWalletActivation( + iconTint = NotificationConfig.IconTint.Attention, + buttonsState = ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = { }, + ), + ), ), bottomSheetConfig = null, tokensListState = textContentTokensState, 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..602de14421 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 @@ -1,17 +1,19 @@ package com.tangem.feature.wallet.presentation.router 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.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.TokenReceiveConfig +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 +77,7 @@ internal class DefaultWalletRouter @Inject constructor( } override fun openStoriesScreen() { - router.push(AppRoute.Home) + router.push(AppRoute.Home()) } override fun isWalletLastScreen(): Boolean { @@ -98,4 +100,10 @@ internal class DefaultWalletRouter @Inject constructor( ), ) } + + override fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) { + dialogNavigation.activate( + configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig), + ) + } } \ No newline at end of file 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..946a3e7692 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,9 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.domain.models.TokenReceiveConfig +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 @@ -54,4 +55,6 @@ internal interface InnerWalletRouter { /** Open NFT collections screen */ fun openNFT(userWallet: UserWallet) + + fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) } \ No newline at end of file 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..a7e173397b 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 @@ -1,6 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce @@ -8,18 +11,20 @@ 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 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.WalletNotification +import com.tangem.utils.extensions.isPositive import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -54,7 +59,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) - addFinishWalletActivationNotification(userWallet, clickIntents) + addFinishWalletActivationNotification(userWallet, maybeTokenList, clickIntents) addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) @@ -256,27 +261,55 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { - if (condition) add(element = element) - } - private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, + maybeTokenList: Lce, clickIntents: WalletClickIntents, ) { if (userWallet !is UserWallet.Hot) return - // TODO [REDACTED_TASK_KEY] set an actual value - val shouldShowFinishActivation = false + val shouldShowFinishActivation = !userWallet.backedUp + + val iconTint = maybeTokenList.fold( + ifLoading = { + if ((it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + IconTint.Warning + } else { + IconTint.Attention + } + }, + ifContent = { + if ((it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + IconTint.Warning + } else { + IconTint.Attention + } + }, + ifError = { IconTint.Attention }, + ) addIf( element = WalletNotification.FinishWalletActivation( - onFinishClick = clickIntents::onFinishWalletActivationClick, + iconTint = iconTint, + buttonsState = when (iconTint) { + IconTint.Warning -> ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = clickIntents::onFinishWalletActivationClick, + ) + else -> ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = clickIntents::onFinishWalletActivationClick, + ) + }, ), condition = shouldShowFinishActivation, ) } + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) add(element = element) + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 } 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/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 1cb651abd1..de8c2ee08a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -90,6 +90,16 @@ internal class WalletStateController @Inject constructor() { ) } + fun hideBottomSheet() { + update( + transformer = OpenBottomSheetTransformer( + userWalletId = getSelectedWalletId(), + content = TangemBottomSheetConfigContent.Empty, + onDismissBottomSheet = {}, + ), + ) + } + private fun getInitialState(): WalletScreenState { return WalletScreenState( topBarConfig = WalletTopBarConfig(onDetailsClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index 2134c40ef5..9bce963c5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.model +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -19,4 +20,7 @@ internal sealed interface WalletDialogConfig { @Serializable data object AskForPushNotifications : WalletDialogConfig + + @Serializable + data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 965b55df30..dd6ae1ec2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -1,7 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference @@ -256,16 +259,15 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class FinishWalletActivation( - val onFinishClick: () -> Unit, + val iconTint: IconTint, + val buttonsState: ButtonsState, ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), subtitle = resourceReference(R.string.hw_activation_need_description), iconResId = R.drawable.img_knight_shield_32, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.hw_activation_need_finish), - onClick = onFinishClick, - ), + iconTint = iconTint, + buttonsState = buttonsState, ), ) @@ -282,6 +284,7 @@ sealed class WalletNotification(val config: NotificationConfig) { text = resourceReference(R.string.notification_referral_promo_button), onClick = onClick, ), + iconSize = 54.dp, ), ) } \ No newline at end of file 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/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 0227a5339a..172ab84fbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme @@ -35,15 +34,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + Notification( + config = it.config, + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + ) + } else -> { Notification( config = it.config, modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), - iconSize = if (it is WalletNotification.ReferralPromo) { - 54.dp - } else { - 20.dp - }, iconTint = when (it) { is WalletNotification.Critical -> TangemTheme.colors.icon.warning is WalletNotification.Informational -> TangemTheme.colors.icon.accent 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/DefaultUserWalletImageFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt new file mode 100644 index 0000000000..f6f11319dd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.utils + +import arrow.core.Either +import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.wallet.utils.UserWalletImageFetcher +import com.tangem.operations.attestation.ArtworkSize +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class DefaultUserWalletImageFetcher @Inject constructor( + private val getCardImageUseCase: GetCardImageUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val artworkUMConverter: ArtworkUMConverter, +) : UserWalletImageFetcher { + + private val smallCache = MutableStateFlow(mapOf()) + private val largeCache = MutableStateFlow(mapOf()) + + override fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow = when (wallet) { + is UserWallet.Cold -> walletImage(wallet.scanResponse.card, size) + is UserWallet.Hot -> flowOf(UserWalletItemUM.ImageState.MobileWallet) + } + + override fun walletsImage( + wallets: Collection, + size: ArtworkSize, + ): Flow> = wallets + .map { userWallet -> walletImage(userWallet, size).map { imageState -> userWallet.walletId to imageState } } + .merge() + .runningFold(mapOf()) { map, newState -> map.plus(newState) } + .filter { it.size >= wallets.size } // prevent spam, waiting full map + .distinctUntilChanged() + + override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow = flow { + val imagesFlow = getUserWalletUseCase.invokeFlow(walletId) + // emit Loading and wait wallet + .onEach { if (it.isLeft()) emit(UserWalletItemUM.ImageState.Loading) } + .filterIsInstance>() + .map { it.value } + .distinctUntilChanged() + .flatMapLatest { wallet -> walletImage(wallet, size) } + emitAll(imagesFlow) + }.distinctUntilChanged() + + override fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow = + internalGetCardImage( + cardInfo = cardDTO, + size = size, + ).distinctUntilChanged() + + private fun internalGetCardImage(cardInfo: CardDTO, size: ArtworkSize): Flow = flow { + emit(cacheOrLoading(cardInfo.cardId, size)) + + val artwork = getCardImageUseCase.invoke( + cardId = cardInfo.cardId, + cardPublicKey = cardInfo.cardPublicKey, + size = size, + manufacturerName = cardInfo.manufacturer.name, + firmwareVersion = cardInfo.firmwareVersion.toSdkFirmwareVersion(), + ) + .let { artworkUMConverter.convert(it) } + .also { save(cardInfo.cardId, size, it) } + emit(UserWalletItemUM.ImageState.Image(artwork)) + } + + private fun cacheOrLoading(cardId: String, size: ArtworkSize): UserWalletItemUM.ImageState { + val artwork = when (size) { + ArtworkSize.LARGE -> largeCache.value[cardId] + ArtworkSize.SMALL -> smallCache.value[cardId] + } + return artwork + ?.let { UserWalletItemUM.ImageState.Image(artwork) } + ?: UserWalletItemUM.ImageState.Loading + } + + private fun save(cardId: String, size: ArtworkSize, artwork: ArtworkUM) { + when (size) { + ArtworkSize.LARGE -> largeCache.update { it.plus(cardId to artwork) } + ArtworkSize.SMALL -> smallCache.update { it.plus(cardId to artwork) } + } + } +} \ 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..6a310e5c31 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 @@ -14,16 +14,15 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.toLce -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.TotalFiatBalance 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.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,18 +42,21 @@ 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, - private val getCardImageUseCase: GetCardImageUseCase, + @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, + @Assisted("authMode") private val authMode: Boolean, + private val userWalletImageFetcher: UserWalletImageFetcher, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { - private var loadedArtworks: HashMap = hashMapOf() private val walletsFlow = if (onlyMultiCurrency) getWalletsUseCase().map { it.filter { it.isMultiCurrency } } else getWalletsUseCase() @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) @@ -63,7 +65,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), - flow4 = loadArtworks(wallets), + flow4 = userWalletImageFetcher.walletsImage(wallets, ArtworkSize.SMALL), ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> createUiModels( wallets = wallets, @@ -86,29 +88,12 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( } .flowOn(dispatchers.default) - private fun loadArtworks(wallets: List): Flow> { - return flow { - emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish - wallets.filterIsInstance().forEach { wallet -> - val artwork = getCardImageUseCase( - cardId = wallet.cardId, - manufacturerName = wallet.scanResponse.card.manufacturer.name, - firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(), - cardPublicKey = wallet.scanResponse.card.cardPublicKey, - size = ArtworkSize.SMALL, - ) - loadedArtworks[wallet.walletId] = artwork - emit(loadedArtworks) - } - } - } - private fun createUiModels( wallets: List, maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, - artworks: HashMap, + artworks: Map, ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, @@ -132,6 +117,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, artwork = artworks[userWallet.walletId], + authMode = authMode, ) .convert(userWallet) } @@ -149,7 +135,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/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index e355a21829..e9ccd6865a 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -11,13 +11,14 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage 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.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.PromoCodeActivationResult import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.usecase.ActivateBitcoinPromocodeUseCase @@ -25,32 +26,17 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.deeplink.DefaultPromoDeeplinkHandler import com.tangem.feature.wallet.deeplink.analytics.PromoActivationAnalytics import com.tangem.feature.wallet.impl.R -import io.mockk.CapturingSlot -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.every +import io.mockk.* import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.mockk -import io.mockk.runs -import io.mockk.slot -import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.test.* import org.junit.After import org.junit.Before import org.junit.Test import timber.log.Timber import java.math.BigDecimal -import com.tangem.domain.wallets.PromoCodeActivationResult class DefaultPromoDeeplinkHandlerTest { @@ -368,6 +354,7 @@ class DefaultPromoDeeplinkHandlerTest { hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val currencyId = CryptoCurrency.ID( 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 5e78c040a8..c5b943b9b4 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 @@ -72,6 +73,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..7557118256 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -13,11 +13,14 @@ android { dependencies { implementation(projects.features.welcome.api) + implementation(projects.features.wallet.api) /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.analytics) implementation(projects.common.routing) implementation(projects.common.ui) @@ -30,6 +33,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 +59,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..3baa86b20e 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,277 @@ 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.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +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.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.features.welcome.impl.R 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, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + 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 = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(R.string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(R.string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(R.string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + onAddWalletOptionClick(optionKey) + }, + ), + onDismissRequest = { + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + }, + ), + ) + } + } + + private fun onAddWalletOptionClick(optionKey: String) { + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + } + + 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 + } + } + } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" + } } \ 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 deleted file mode 100644 index 4331461929..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -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.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM - -@Composable -fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - titleText = TextReference.Str("Add Wallet"), - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: AddWalletBottomSheetContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - InputRowDefault( - text = TextReference.Str("Create New Wallet"), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 0, - lastIndex = 3, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, - ) - InputRowDefault( - text = TextReference.Str("Add Existing Wallet"), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 1, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, - ) - InputRowDefault( - text = TextReference.Str("Buy Tangem Wallet"), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 2, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) }, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - AddWalletBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = AddWalletBottomSheetContentUM(), - ), - ) - } -} \ No newline at end of file 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..7e9e671696 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,18 @@ 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.R.* import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -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.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 +46,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 +63,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 +130,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 +154,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 +169,18 @@ 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, - ) - } - } +fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) } \ 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/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..d785e00c38 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,27 @@ 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") + +include(":features:token-recieve:api") +include(":features:token-recieve:impl") // endregion Feature modules // region Domain modules @@ -273,6 +288,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 +346,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