diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3682efb9f5..d8615f2502 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,8 +42,8 @@ configurations.all { dependencies { - implementation(files("libs/walletconnect-1.5.6.aar")) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.core) implementation(projects.domain.card) @@ -67,6 +67,8 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning.models) + implementation(projects.domain.staking) + implementation(projects.domain.walletConnect) implementation(projects.common) implementation(projects.core.analytics) @@ -77,9 +79,11 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.core.decompose) implementation(projects.core.deepLinks) implementation(projects.libs.crypto) implementation(projects.libs.auth) + implementation(projects.libs.blockchainSdk) implementation(projects.data.appCurrency) implementation(projects.data.appTheme) @@ -87,7 +91,6 @@ dependencies { implementation(projects.data.card) implementation(projects.data.common) implementation(projects.data.settings) - implementation(projects.data.source.preferences) implementation(projects.data.tokens) implementation(projects.data.txhistory) implementation(projects.data.wallets) @@ -98,6 +101,8 @@ dependencies { implementation(projects.data.onboarding) implementation(projects.data.feedback) implementation(projects.data.qrScanning) + implementation(projects.data.staking) + implementation(projects.data.walletConnect) /** Features */ implementation(projects.features.onboarding) @@ -121,6 +126,10 @@ dependencies { implementation(projects.features.send.impl) implementation(projects.features.qrScanning.api) implementation(projects.features.qrScanning.impl) + implementation(projects.features.staking.api) + implementation(projects.features.staking.impl) + implementation(projects.features.details.api) + implementation(projects.features.details.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -183,7 +192,6 @@ dependencies { implementation(deps.reKotlin) implementation(deps.zxing.qrCore) implementation(deps.coil) - implementation(deps.appsflyer) implementation(deps.amplitude) implementation(deps.kotsonGson) implementation(deps.spongecastle.core) @@ -216,9 +224,12 @@ dependencies { androidTestImplementation(deps.test.kaspresso.compose) androidTestImplementation(deps.test.compose.junit) androidTestImplementation(deps.test.hamcrest) + androidTestImplementation(deps.test.hilt) + kaptAndroidTest(deps.test.hilt.compiler) /** Chucker */ debugImplementation(deps.chucker) + mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt new file mode 100644 index 0000000000..e1c23e8e9b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -0,0 +1,28 @@ +package com.tangem.common + +import androidx.test.core.app.ApplicationProvider +import com.tangem.tap.ApplicationEntryPoint +import com.tangem.tap.TangemApplication +import dagger.hilt.android.testing.OnComponentReadyRunner +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class ApplicationInjectionExecutionRule : TestRule { + + private val tangemApplication: TangemApplication + get() = ApplicationProvider.getApplicationContext() + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + OnComponentReadyRunner.addListener( + tangemApplication, ApplicationEntryPoint::class.java + ) { _: ApplicationEntryPoint -> + tangemApplication.init() + } + base.evaluate() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt new file mode 100644 index 0000000000..4b3278bc9f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -0,0 +1,56 @@ +package com.tangem.common + +import android.Manifest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.espresso.intent.Intents +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import com.kaspersky.components.composesupport.config.withComposeSupport +import com.kaspersky.kaspresso.kaspresso.Kaspresso +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.tangem.tap.MainActivity +import com.tangem.tap.domain.sdk.TangemSdkManager +import dagger.hilt.android.testing.HiltAndroidRule +import org.junit.Rule +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import javax.inject.Inject + +@RunWith(AndroidJUnit4::class) +abstract class BaseTestCase : TestCase( + kaspressoBuilder = Kaspresso.Builder.withComposeSupport() +) { + + @Inject + lateinit var tangemSdkManager: TangemSdkManager + + @get:Rule + open val composeTestRule = createAndroidComposeRule() + + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.CAMERA + ) + + private val hiltRule = HiltAndroidRule(this) + + @Rule + @JvmField + val ruleChain = RuleChain + .outerRule(hiltRule) + .around(ApplicationInjectionExecutionRule()) + + protected fun setupHooks( + additionalBeforeSection: () -> Unit = {}, + additionalAfterSection: () -> Unit = {}, + ) = before { + hiltRule.inject() + Intents.init() + additionalBeforeSection() + }.after { + additionalAfterSection() + Intents.release() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt b/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt new file mode 100644 index 0000000000..3f5c02d5a5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt @@ -0,0 +1,17 @@ +package com.tangem.common + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import com.tangem.common.di.TangemMockedApplication_Application + +class HiltTestRunner : AndroidJUnitRunner() { + + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, TangemMockedApplication_Application::class.java.name, context) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/TangemEmptyApplication.kt b/app/src/androidTest/kotlin/com/tangem/common/TangemEmptyApplication.kt new file mode 100644 index 0000000000..2c1cb02a42 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/TangemEmptyApplication.kt @@ -0,0 +1,11 @@ +package com.tangem.common + +import com.tangem.tap.TangemApplication + +open class TangemEmptyApplication : TangemApplication() { + + override fun onCreate() { + // super.onCreate() is not called intentionally + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/di/TangemMockedApplication.kt b/app/src/androidTest/kotlin/com/tangem/common/di/TangemMockedApplication.kt new file mode 100644 index 0000000000..06698555e9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/di/TangemMockedApplication.kt @@ -0,0 +1,7 @@ +package com.tangem.common.di + +import com.tangem.common.TangemEmptyApplication +import dagger.hilt.android.testing.CustomTestApplication + +@CustomTestApplication(TangemEmptyApplication::class) +internal class TangemMockedApplication \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/di/TestModule.kt b/app/src/androidTest/kotlin/com/tangem/common/di/TestModule.kt new file mode 100644 index 0000000000..106c04012d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/di/TestModule.kt @@ -0,0 +1,28 @@ +package com.tangem.common.di + +import android.content.Context +import com.tangem.tap.di.TangemSdkManagerModule +import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [TangemSdkManagerModule::class] +) +object TestModule { + + @Provides + @Singleton + fun provideTangemSdkManager( + @ApplicationContext context: Context + ): TangemSdkManager { + return MockTangemSdkManager(resources = context.resources) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt b/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt deleted file mode 100644 index 6e0bfc19b2..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.helpers.base - -import android.Manifest -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.GrantPermissionRule -import com.kaspersky.components.composesupport.config.withComposeSupport -import com.kaspersky.kaspresso.kaspresso.Kaspresso -import com.kaspersky.kaspresso.testcases.api.testcase.TestCase -import com.tangem.tap.MainActivity -import org.junit.Rule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -open class BaseAutoTestCase : TestCase( - kaspressoBuilder = Kaspresso.Builder.withComposeSupport() -) { - - @get:Rule - open val composeTestRule = createAndroidComposeRule() - - @get: Rule - val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( - Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.CAMERA - ) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt new file mode 100644 index 0000000000..51b94ab791 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt @@ -0,0 +1,17 @@ +package com.tangem.screens + +import com.kaspersky.kaspresso.screens.KScreen +import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment +import com.tangem.wallet.R +import io.github.kakaocup.kakao.text.KButton + +object DisclaimerScreen : KScreen(){ + + override val layoutId = R.layout.fragment_disclaimer + + override val viewClass = DisclaimerFragment::class.java + + val acceptButton: KButton = KButton { + withId(R.id.btn_accept) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt index 38bdbce846..cda17d33c2 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt @@ -1,7 +1,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import com.tangem.tap.common.compose.resources.C +import com.tangem.core.ui.test.TestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.KNode @@ -11,15 +11,15 @@ import io.github.kakaocup.kakao.text.KButton class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen( semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag(C.Tag.STORIES_SCREEN) } + viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) } ) { val scanButton: KNode = child { - hasTestTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON) + hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON) } val orderButton: KNode = child { - hasTestTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON) + hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON) } val enableNFCAlert: KView = KView { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt new file mode 100644 index 0000000000..0988625537 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt @@ -0,0 +1,11 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen + +class WalletScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.WALLET_SCREEN) } + ) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt new file mode 100644 index 0000000000..dcebde4556 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt @@ -0,0 +1,39 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.screens.DisclaimerScreen +import com.tangem.screens.StoriesScreen +import com.tangem.screens.WalletScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen +import org.junit.Test + +@HiltAndroidTest +class MainScreenTest : BaseTestCase() { + + @Test + fun goToMain() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button") { + scanButton { + assertIsDisplayed() + performClick() + } + } + } + DisclaimerScreen { + step("Click on \"Accept\" button") { + acceptButton { + isVisible() + click() + } + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Make sure wallet screen is visible") { + assertIsDisplayed() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt new file mode 100644 index 0000000000..a1d67d8c1c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt @@ -0,0 +1,48 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.screens.DisclaimerScreen +import com.tangem.screens.StoriesScreen +import com.tangem.screens.WalletScreen +import com.tangem.tap.domain.sdk.mocks.MockProvider +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen +import org.junit.Test + +@HiltAndroidTest +class ScanErrorTest : BaseTestCase() { + + @Test + fun goToMain() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button emulating scan error") { + MockProvider.setEmulateError() + scanButton { + assertIsDisplayed() + performClick() + } + } + step("Click on \"Scan\" button again without emulating error") { + MockProvider.resetEmulateError() + scanButton { + assertIsDisplayed() + performClick() + } + } + } + DisclaimerScreen { + step("Click on \"Accept\" button") { + acceptButton { + isVisible() + click() + } + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Make sure wallet screen is visible") { + 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 ce58b4a1be..d6418c9364 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -1,45 +1,43 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW -import androidx.test.espresso.intent.Intents -import com.tangem.helpers.base.BaseAutoTestCase +import com.tangem.common.BaseTestCase import com.tangem.screens.StoriesScreen import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL +import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.kakao.intent.KIntent import org.junit.Test -class StoriesTest : BaseAutoTestCase() { +@HiltAndroidTest +class StoriesTest : BaseTestCase() { @Test - fun clickOnButtons() = before { - Intents.init() - }.after { - Intents.release() - }.run { - ComposeScreen.onComposeScreen(composeTestRule) { - step("Click on \"Scan\" button") { - scanButton { - assertIsDisplayed() - performClick() + fun clickOnButtons() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button") { + scanButton { + assertIsDisplayed() + performClick() + } } - } - step("Assert: \"Scan card\" popup opened") { - enableNFCAlert.isDisplayed() - cancelButton.click() - device.uiDevice.pressBack() - } - step("Click on \"Order\" button") { - orderButton.performClick() - } - step("Assert: browser opened") { - val expectedIntent = KIntent { - hasAction(ACTION_VIEW) - hasData(NEW_BUY_WALLET_URL) + step("Assert: \"Scan card\" popup opened") { + enableNFCAlert.isDisplayed() + cancelButton.click() + device.uiDevice.pressBack() + } + step("Click on \"Order\" button") { + orderButton.performClick() + } + step("Assert: browser opened") { + val expectedIntent = KIntent { + hasAction(ACTION_VIEW) + hasData(NEW_BUY_WALLET_URL) + } + expectedIntent.intended() + device.uiDevice.pressBack() } - expectedIntent.intended() - device.uiDevice.pressBack() } } - } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 093b3eb247..1d52735ce7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ - @@ -36,7 +35,7 @@ @@ -170,11 +172,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + val splashScreen = installSplashScreen() + installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation super.onCreate(savedInstanceState) + splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } + installActivityDependencies() observeAppThemeModeUpdates() @@ -217,7 +222,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(cardSdkConfigRepository.sdk, this) - lockUserWalletsTimer = LockUserWalletsTimer(owner = this) + lockUserWalletsTimer = LockUserWalletsTimer( + owner = this, + settingsRepository = settingsRepository, + userWalletsListManager = userWalletsListManager, + ) initIntentHandlers() @@ -232,6 +241,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac cardSdkConfigRepository = cardSdkConfigRepository, sendRouter = sendRouter, qrScanningRouter = qrScanningRouter, + emailSender = emailSender, ), ) } @@ -264,9 +274,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun createAppThemeModeFlow(): SharedFlow { - val tapApplication = application as TapApplication + val tangemApplication = application as TangemApplication - return tapApplication.getAppThemeModeUseCase() + return tangemApplication.getAppThemeModeUseCase() .map { maybeMode -> maybeMode.getOrElse { AppThemeMode.DEFAULT } } @@ -283,12 +293,31 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() + val nfcAdapter = NfcAdapter.getDefaultAdapter(this) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + val intentFilters = arrayOf( + IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), + ) + nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) navigateToInitialScreenIfNeeded(intent) } + override fun onPause() { + super.onPause() + val nfcAdapter = NfcAdapter.getDefaultAdapter(this) + nfcAdapter.disableForegroundDispatch(this) + } + override fun onStop() { notificationsHandler = null dialogManager.onStop() @@ -303,7 +332,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun initIntentHandlers() { - val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } + val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) } @@ -341,7 +370,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac cardSdkLifecycleObserver.onCreate(context = this) lifecycleScope.launch { - intentProcessor.handleIntent(intent) + intentProcessor.handleIntent(intent, true) } if (intent != null) { @@ -437,15 +466,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { - val canSaveWallets = if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { - runCatching { userWalletsListManager.asLockable()?.isLockedSync } - .fold(onSuccess = { true }, onFailure = { false }) - } else { - userWalletsListManager is BiometricUserWalletsListManager - } - val hasSavedWallets = userWalletsListManager.hasUserWallets - - if (canSaveWallets && hasSavedWallets) { + if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatch( NavigationAction.NavigateTo( screen = AppScreen.Welcome, @@ -457,7 +478,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } else { store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) lifecycleScope.launch { - intentProcessor.handleIntent(intentWhichStartedActivity) + intentProcessor.handleIntent(intentWhichStartedActivity, false) } } diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt similarity index 64% rename from app/src/main/java/com/tangem/tap/TapApplication.kt rename to app/src/main/java/com/tangem/tap/TangemApplication.kt index 7c26fe7570..73e4bcd76f 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -11,17 +11,14 @@ import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat import com.tangem.TangemSdkLogger -import com.tangem.blockchain.common.AccountCreator -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder +import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config @@ -35,20 +32,24 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetFeedbackEmailUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.details.DetailsEntryPoint +import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler -import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo @@ -61,134 +62,140 @@ import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.product.DerivationsFinder -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation -import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.wallet.BuildConfig -import dagger.hilt.android.HiltAndroidApp +import dagger.hilt.EntryPoints import kotlinx.coroutines.runBlocking import org.rekotlin.Store import timber.log.Timber -import javax.inject.Inject -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository lateinit var store: Store lateinit var foregroundActivityObserver: ForegroundActivityObserver lateinit var activityResultCaller: ActivityResultCaller -lateinit var preferencesStorage: PreferencesDataSource -lateinit var walletConnectRepository: WalletConnectRepository internal lateinit var derivationsFinder: DerivationsFinder -@HiltAndroidApp -internal class TapApplication : Application(), ImageLoaderFactory { +abstract class TangemApplication : Application(), ImageLoaderFactory { - // region Injected - @Inject - lateinit var appStateHolder: AppStateHolder + // region DI + private val entryPoint: ApplicationEntryPoint + get() = EntryPoints.get(this, ApplicationEntryPoint::class.java) - @Inject - lateinit var configManager: ConfigManager + private val appStateHolder: AppStateHolder + get() = entryPoint.getAppStateHolder() - @Inject - lateinit var assetReader: AssetReader + private val configManager: ConfigManager + get() = entryPoint.getConfigManager() - @Inject - lateinit var featureTogglesManager: FeatureTogglesManager + private val assetLoader: AssetLoader + get() = entryPoint.getAssetLoader() - @Inject - lateinit var networkConnectionManager: NetworkConnectionManager + private val featureTogglesManager: FeatureTogglesManager + get() = entryPoint.getFeatureTogglesManager() - @Inject - lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles + private val networkConnectionManager: NetworkConnectionManager + get() = entryPoint.getNetworkConnectionManager() - @Inject - lateinit var preferencesDataSource: PreferencesDataSource + private val customTokenFeatureToggles: CustomTokenFeatureToggles + get() = entryPoint.getCustomTokenFeatureToggles() - @Inject - lateinit var walletConnect2Repository: WalletConnect2Repository + private val walletConnect2Repository: WalletConnect2Repository + get() = entryPoint.getWalletConnect2Repository() - @Inject - lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository + private val walletConnectSessionsRepository: WalletConnectSessionsRepository + get() = entryPoint.getWalletConnectSessionsRepository() - @Inject - lateinit var manageTokensFeatureToggles: ManageTokensFeatureToggles + private val manageTokensFeatureToggles: ManageTokensFeatureToggles + get() = entryPoint.getManageTokensFeatureToggles() - @Inject - lateinit var scanCardProcessor: ScanCardProcessor + private val scanCardProcessor: ScanCardProcessor + get() = entryPoint.getScanCardProcessor() - @Inject - lateinit var appCurrencyRepository: AppCurrencyRepository + private val appCurrencyRepository: AppCurrencyRepository + get() = entryPoint.getAppCurrencyRepository() - @Inject - lateinit var walletManagersFacade: WalletManagersFacade + private val walletManagersFacade: WalletManagersFacade + get() = entryPoint.getWalletManagersFacade() - @Inject - lateinit var networksRepository: NetworksRepository + private val networksRepository: NetworksRepository + get() = entryPoint.getNetworksRepository() - @Inject - lateinit var currenciesRepository: CurrenciesRepository + private val currenciesRepository: CurrenciesRepository + get() = entryPoint.getCurrenciesRepository() - @Inject - lateinit var appThemeModeRepository: AppThemeModeRepository + private val appThemeModeRepository: AppThemeModeRepository + get() = entryPoint.getAppThemeModeRepository() - @Inject - lateinit var balanceHidingRepository: BalanceHidingRepository + private val balanceHidingRepository: BalanceHidingRepository + get() = entryPoint.getBalanceHidingRepository() - @Inject - lateinit var userTokensStore: UserTokensStore + private val userTokensStore: UserTokensStore + get() = entryPoint.getUserTokensStore() - @Inject - lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase + val getAppThemeModeUseCase: GetAppThemeModeUseCase + get() = entryPoint.getGetAppThemeModeUseCase() - @Inject - lateinit var walletsRepository: WalletsRepository + private val walletsRepository: WalletsRepository + get() = entryPoint.getWalletsRepository() - @Inject - lateinit var sendFeatureToggles: SendFeatureToggles + private val sendFeatureToggles: SendFeatureToggles + get() = entryPoint.getSendFeatureToggles() - @Inject - lateinit var oneTimeEventFilter: OneTimeEventFilter + private val oneTimeEventFilter: OneTimeEventFilter + get() = entryPoint.getOneTimeEventFilter() - @Inject - lateinit var blockchainDataStorage: BlockchainDataStorage + private val generalUserWalletsListManager: UserWalletsListManager + get() = entryPoint.getGeneralUserWalletsListManager() - @Inject - lateinit var accountCreator: AccountCreator + private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase + get() = entryPoint.getWasTwinsOnboardingShownUseCase() - @Inject - lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles + private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase + get() = entryPoint.getSaveTwinsOnboardingShownUseCase() - @Inject - lateinit var generalUserWalletsListManager: UserWalletsListManager + private val generateWalletNameUseCase: GenerateWalletNameUseCase + get() = entryPoint.getWalletNameGenerateUseCase() - @Inject - lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase + private val cardRepository: CardRepository + get() = entryPoint.getCardRepository() - @Inject - lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + get() = entryPoint.getFeedbackManagerFeatureToggles() - @Inject - lateinit var cardRepository: CardRepository + private val tangemSdkLogger: TangemSdkLogger + get() = entryPoint.getTangemSdkLogger() - @Inject - lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + private val settingsRepository: SettingsRepository + get() = entryPoint.getSettingsRepository() - @Inject - lateinit var blockchainSDKLogger: BlockchainSDKLogger + private val blockchainSDKFactory: BlockchainSDKFactory + get() = entryPoint.getBlockchainSDKFactory() - @Inject - lateinit var tangemSdkLogger: TangemSdkLogger - // endregion Injected + private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase + get() = entryPoint.getGetFeedbackEmailUseCase() + + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase + get() = entryPoint.getSaveBlockchainErrorUseCase() + // endregion + + private val detailsFeatureToggles: DetailsFeatureToggles + get() = entryPoint.getDetailsFeatureToggles() + + private val detailsEntryPoint: DetailsEntryPoint + get() = entryPoint.getDetailsEntryPoint() override fun onCreate() { super.onCreate() + init() + } + + fun init() { store = createReduxStore() if (BuildConfig.LOG_ENABLED) { @@ -206,23 +213,16 @@ internal class TapApplication : Application(), ImageLoaderFactory { activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - preferencesStorage = preferencesDataSource - walletConnectRepository = WalletConnectRepository(this) - // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { featureTogglesManager.init() - - if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { - store.dispatch(GlobalAction.UpdateUserWalletsListManager(generalUserWalletsListManager)) - } else { - initUserWalletsListManager() - } + initConfigManager( + loader = FeaturesLocalLoader(assetLoader, BuildConfig.ENVIRONMENT), + onComplete = ::initWithConfigDependency, + ) } - val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) - initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() loadNativeLibraries() @@ -264,16 +264,19 @@ internal class TapApplication : Application(), ImageLoaderFactory { balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, sendFeatureToggles = sendFeatureToggles, - blockchainDataStorage = blockchainDataStorage, - accountCreator = accountCreator, - userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles, generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, + generateWalletNameUseCase = generateWalletNameUseCase, cardRepository = cardRepository, feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, tangemSdkLogger = tangemSdkLogger, - blockchainSDKLogger = blockchainSDKLogger, + settingsRepository = settingsRepository, + blockchainSDKFactory = blockchainSDKFactory, + saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, + detailsFeatureToggles = detailsFeatureToggles, + detailsEntryPoint = detailsEntryPoint, + assetLoader = assetLoader, ), ), ) @@ -290,7 +293,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { System.loadLibrary("TrustWalletCore") } - private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) { + private suspend fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) { configManager.load(loader) { config -> store.dispatch(GlobalAction.SetConfigManager(configManager)) onComplete(config) @@ -305,7 +308,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { private fun initAnalytics(application: Application, config: Config) { val factory = AnalyticsFactory() factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) - factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addFilter(oneTimeEventFilter) @@ -366,6 +368,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, chatManager = ChatManager(foregroundActivityObserver), + feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + getFeedbackEmailUseCase = getFeedbackEmailUseCase, ) store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) } @@ -373,14 +377,4 @@ internal class TapApplication : Application(), ImageLoaderFactory { private fun initWarningMessagesManager() { store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) } - - private suspend fun initUserWalletsListManager() { - val manager = if (walletsRepository.shouldSaveUserWalletsSync()) { - UserWalletsListManager.provideBiometricImplementation(applicationContext) - } else { - UserWalletsListManager.provideRuntimeImplementation() - } - - store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemHiltApplication.kt b/app/src/main/java/com/tangem/tap/TangemHiltApplication.kt new file mode 100644 index 0000000000..a818249392 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/TangemHiltApplication.kt @@ -0,0 +1,6 @@ +package com.tangem.tap + +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class TangemHiltApplication : TangemApplication() \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 5887deeadc..6b870386be 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -86,10 +86,6 @@ class DialogManager : StoreSubscriber { context = context, ) } - is WalletConnectDialog.ApproveWcSession -> - ApproveWcSessionDialog.create(state.dialog.session, state.dialog.networks, context) - is WalletConnectDialog.ChooseNetwork -> - ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context) is WalletConnectDialog.ClipboardOrScanQr -> ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context) is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 489effa20d..8cfba8dd64 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -13,20 +13,20 @@ sealed class AnalyticsParam { } sealed class CardBalanceState(val value: String) { - object Empty : CardBalanceState("Empty") - object Full : CardBalanceState("Full") + data object Empty : CardBalanceState("Empty") + data object Full : CardBalanceState("Full") companion object } sealed class RateApp(val value: String) { - object Liked : RateApp("Liked") - object Closed : RateApp("Close") + data object Liked : RateApp("Liked") + data object Closed : RateApp("Close") } sealed class OnOffState(val value: String) { - object On : OnOffState("On") - object Off : OnOffState("Off") + data object On : OnOffState("On") + data object Off : OnOffState("Off") companion object { @@ -35,13 +35,13 @@ sealed class AnalyticsParam { } sealed class UserCode(val value: String) { - object AccessCode : UserCode("Access Code") + data object AccessCode : UserCode("Access Code") } sealed class SecurityMode(val value: String) { - object AccessCode : SecurityMode("Access Code") - object Passcode : SecurityMode("Passcode") - object LongTap : SecurityMode("Long Tap") + data object AccessCode : SecurityMode("Access Code") + data object Passcode : SecurityMode("Passcode") + data object LongTap : SecurityMode("Long Tap") companion object { fun from(option: SecurityOption): SecurityMode = when (option) { @@ -56,8 +56,8 @@ sealed class AnalyticsParam { val key: String = "Status" - object Enabled : AccessCodeRecoveryStatus("Enabled") - object Disabled : AccessCodeRecoveryStatus("Disabled") + data object Enabled : AccessCodeRecoveryStatus("Enabled") + data object Disabled : AccessCodeRecoveryStatus("Disabled") companion object { fun from(enabled: Boolean): AccessCodeRecoveryStatus { @@ -67,21 +67,21 @@ sealed class AnalyticsParam { } sealed class Error(val value: String) { - object App : Error("App Error") - object CardSdk : Error("Card Sdk Error") - object BlockchainSdk : Error("Blockchain Sdk Error") + data object App : Error("App Error") + data object CardSdk : Error("Card Sdk Error") + data object BlockchainSdk : Error("Blockchain Sdk Error") } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType(value = "Private Key") - object NewSeed : WalletCreationType(value = "New Seed") - object SeedImport : WalletCreationType(value = "Seed Import") + data object PrivateKey : WalletCreationType(value = "Private Key") + data object NewSeed : WalletCreationType(value = "New Seed") + data object SeedImport : WalletCreationType(value = "Seed Import") } sealed class AppTheme(val value: String) { - object System : AppTheme("System") - object Dark : AppTheme("Dark") - object Light : AppTheme("Light") + data object System : AppTheme("System") + data object Dark : AppTheme("Dark") + data object Light : AppTheme("Light") companion object { fun fromAppThemeMode(mode: AppThemeMode): AppTheme { @@ -104,6 +104,7 @@ sealed class AnalyticsParam { const val PERMISSION_TYPE = "Permission Type" const val PRODUCT_TYPE = "Product Type" const val FIRMWARE = "Firmware" + const val USER_WALLET_ID = "User Wallet ID" const val CURRENCY = "Currency" const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt index 969d1d47fb..9bf97f2250 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.events +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.extensions.filterNotNull diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index b9a831cb3d..8ba8e26539 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -36,11 +36,19 @@ sealed class Settings( ) : Settings("Settings / Card Settings", event, params, error) { class ButtonFactoryReset : CardSettings("Button - Factory Reset") - class FactoryResetFinished(error: Throwable? = null) : CardSettings( + class FactoryResetFinished(cardsCount: Int? = null, error: Throwable? = null) : CardSettings( event = "Factory Reset Finished", + params = buildMap { + cardsCount?.let { put("Cards Count", "$it") } + }, error = error, ) + class FactoryResetCanceled(cardsCount: Int) : CardSettings( + event = "Factory Reset Canceled", + params = mapOf("Cards Count" to "$cardsCount"), + ) + class UserCodeChanged : CardSettings("User Code Changed") class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt deleted file mode 100644 index 710c7308e6..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.tap.common.analytics.handlers.appsFlyer - -import com.appsflyer.AFInAppEventType -import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder -import com.tangem.tap.common.analytics.events.Shop - -class AppsFlyerAnalyticsHandler( - private val client: AppsFlyerAnalyticsClient, -) : AnalyticsHandler { - - override fun id(): String = ID - - override fun send(eventId: String, params: Map) { - client.logEvent(eventId, params) - } - - override fun send(event: AnalyticsEvent) { - if (event is Shop.Purchased) { - send(AFInAppEventType.PURCHASE, event.params) - } else { - super.send(event) - } - } - - companion object { - const val ID = "AppsFlyer" - } - - class Builder : AnalyticsHandlerBuilder { - override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when { - !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerDevKey) - data.isDebug && data.logConfig.appsFlyer -> AppsFlyerLogClient(data.jsonConverter) - else -> null - }?.let { AppsFlyerAnalyticsHandler(it) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt deleted file mode 100644 index 40cee4e698..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.common.analytics.handlers.appsFlyer - -import android.content.Context -import com.appsflyer.AppsFlyerLib -import com.tangem.core.analytics.api.EventLogger - -/** -[REDACTED_AUTHOR] - */ -interface AppsFlyerAnalyticsClient : EventLogger - -internal class AppsFlyerClient( - private val context: Context, - key: String, -) : AppsFlyerAnalyticsClient { - - private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() - - init { - appsFlyerLib.init(key, null, context) - appsFlyerLib.start(context) - } - - override fun logEvent(event: String, params: Map) { - appsFlyerLib.logEvent(context, event, params) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt deleted file mode 100644 index 8e87061b2c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.common.analytics.handlers.appsFlyer - -import com.tangem.common.json.MoshiJsonConverter -import com.tangem.tap.common.analytics.AnalyticsEventsLogger - -/** -[REDACTED_AUTHOR] - */ -internal class AppsFlyerLogClient( - jsonConverter: MoshiJsonConverter, -) : AppsFlyerAnalyticsClient { - - private val logger = AnalyticsEventsLogger(AppsFlyerAnalyticsHandler.ID, jsonConverter) - - override fun logEvent(event: String, params: Map) { - logger.logEvent(event, params) - } -} \ 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 c881cd0127..e7c6feee04 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 @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.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 @@ -18,6 +19,8 @@ class CardContextInterceptor( private val scanResponse: ScanResponse, ) : ParamsInterceptor { + private val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + override fun id(): String = CardContextInterceptor.id() override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { @@ -32,6 +35,9 @@ class CardContextInterceptor( params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse) params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue + if (userWalletId != null) { + params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue + } ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { params[AnalyticsParam.CURRENCY] = it.value diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt new file mode 100644 index 0000000000..8b7ee07933 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.clipboard + +import android.content.ClipData +import com.tangem.core.ui.clipboard.ClipboardManager +import android.content.ClipboardManager as AndroidClipboardManager + +internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager { + + override fun setText(label: String, text: String) { + clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt new file mode 100644 index 0000000000..bdab75088f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.clipboard + +import com.tangem.core.ui.clipboard.ClipboardManager +import timber.log.Timber + +internal object MockClipboardManager : ClipboardManager { + override fun setText(label: String, text: String) { + /** Intentionnaly do nothing */ + Timber.w("Clipboard Manager not available") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt b/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt deleted file mode 100644 index ca77ba62d2..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.common.compose.resources - -object C { - object Tag { - const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" - const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" - const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index f6c299acb1..4b0a740760 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -137,7 +137,15 @@ private fun fragmentFactory(screen: AppScreen): Fragment { SendFragment() } } - AppScreen.Details -> DetailsFragment() + AppScreen.Details -> { + val featureToggles = store.inject(getDependency = DaggerGraphState::detailsFeatureToggles) + + if (featureToggles.isRedesignEnabled) { + store.inject(DaggerGraphState::detailsEntryPoint).entryFragment() + } else { + DetailsFragment() + } + } AppScreen.DetailsSecurity -> SecurityModeFragment() AppScreen.CardSettings -> CardSettingsFragment() AppScreen.AppSettings -> AppSettingsFragment() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index f4df731c74..5347ee2efc 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.utils.amountToCreateAccount import com.tangem.common.services.Result -import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.TestActions import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 21f7717f88..21fc321bbe 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.address.Address import com.tangem.crypto.NetworkType import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.extensions.stripZeroPlainString import java.util.concurrent.CopyOnWriteArrayList diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 83a14893d8..98aa013e84 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -1,13 +1,22 @@ package com.tangem.tap.common.feedback import android.content.Context +import com.tangem.core.navigation.email.EmailSender import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.TapWorkarounds +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.tap.common.chat.ChatManager +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.mainScope +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import com.tangem.tap.withForegroundActivity +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import java.io.FileWriter @@ -20,21 +29,46 @@ class FeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val chatManager: ChatManager, + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, + private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase, ) { private var sessionFeedbackFile: File? = null private var sessionLogsFile: File? = null fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { - feedbackData.prepare(infoHolder) - foregroundActivityObserver.withForegroundActivity { activity -> - activity.sendEmail( - email = getSupportEmail(), - subject = activity.getString(feedbackData.subjectResId), - message = feedbackData.joinTogether(activity, infoHolder), - file = getLogFile(activity), - onFail = onFail, - ) + if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { + mainScope.launch { + val email = getFeedbackEmailUseCase( + when (feedbackData) { + is FeedbackEmail -> FeedbackEmailType.DirectUserRequest + is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter + is ScanFailsEmail -> FeedbackEmailType.ScanningProblem + is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem + else -> FeedbackEmailType.DirectUserRequest + }, + ) + + store.inject(DaggerGraphState::emailSender).send( + email = EmailSender.Email( + address = email.address, + subject = email.subject, + message = email.message, + attachment = email.file, + ), + ) + } + } else { + feedbackData.prepare(infoHolder) + foregroundActivityObserver.withForegroundActivity { activity -> + activity.sendEmail( + email = getSupportEmail(), + subject = activity.getString(feedbackData.subjectResId), + message = feedbackData.joinTogether(activity, infoHolder), + file = getLogFile(activity), + onFail = onFail, + ) + } } } diff --git a/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt b/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt new file mode 100644 index 0000000000..688dea7f67 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.common.haptic + +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import com.tangem.core.ui.haptic.HapticManager + +class DefaultHapticManager(private val vibrator: Vibrator) : HapticManager { + + override fun vibrateShort() { + vibrate(VIBRATION_SHORT_DURATION) + } + + override fun vibrateMeduim() { + vibrate(VIBRATION_MEDIUM_LOW_DURATION) + } + + override fun vibrateLong() { + vibrate(VIBRATION_MEDIUM_DURATION) + } + + private fun vibrate(durationMs: Long) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + vibrator.vibrate(durationMs) + } + } + + companion object { + const val VIBRATION_SHORT_DURATION = 50L + const val VIBRATION_MEDIUM_LOW_DURATION = 75L + const val VIBRATION_MEDIUM_DURATION = 100L + const val VIBRATION_LONG_DURATION = 200L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt index c8fa6db166..57be8c6e21 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt @@ -3,9 +3,10 @@ package com.tangem.tap.common.redux import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.preferencesStorage +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store +import kotlinx.coroutines.launch import org.rekotlin.Middleware class AccessCodeRequestPolicyMiddleware { @@ -21,8 +22,12 @@ class AccessCodeRequestPolicyMiddleware { } private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) { - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, - ) + mainScope.launch { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a9c23d448f..8439279a0a 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.navigation.StateDialog @@ -9,7 +8,6 @@ import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.feedback.FeedbackData import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.redux.DebugErrorAction @@ -85,7 +83,6 @@ sealed class GlobalAction : Action { data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() - data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { object Init : GlobalAction() { @@ -101,6 +98,5 @@ sealed class GlobalAction : Action { data class Success(val countryCode: String) : GlobalAction() } - data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction() data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 921898e383..fc584f6b8e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -3,9 +3,7 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.models.Config import com.tangem.domain.appcurrency.model.AppCurrency @@ -15,7 +13,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.mainScope import com.tangem.tap.network.exchangeServices.BuyExchangeService import com.tangem.tap.network.exchangeServices.CardExchangeRules import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -26,13 +23,10 @@ import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware -import timber.log.Timber import java.util.Locale object GlobalMiddleware { @@ -97,16 +91,13 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } feedbackManager.openChat(chatConfig, action.feedbackData) } - is GlobalAction.UpdateFeedbackInfo -> { - store.state.globalState.feedbackManager?.infoHolder - ?.setWalletsInfo(action.walletManagers) - } is GlobalAction.ExchangeManager.Init -> { val appStateSafe = appState() ?: return val config = appStateSafe.globalState.configManager?.config ?: return scope.launch { val scanResponseProvider: () -> ScanResponse? = { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.selectedUserWalletSync?.scanResponse } val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card } @@ -148,33 +139,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } } } - is GlobalAction.UpdateUserWalletsListManager -> { - val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade) - - /* - * If implementation of the UserWalletsListManager is changed, - * then all observers of selectedUserWallet become irrelevant. - */ - action.manager.selectedUserWallet - .distinctUntilChanged() - .onEach { userWallet -> - Analytics.setContext(userWallet.scanResponse) - Analytics.send(Basic.WalletOpened()) - - store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> - infoHolder.setCardInfo(userWallet.scanResponse) - - walletManagersFacade - .getAll(userWallet.walletId) - .distinctUntilChanged() - .onEach(infoHolder::setWalletsInfo) - .catch { Timber.e(it) } - .launchIn(mainScope) - } - } - .flowOn(Dispatchers.IO) - .launchIn(scope) - } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index fbe3e38b89..d85d35f457 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -3,12 +3,10 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.proxy.AppStateHolder -import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.extensions.replaceBy import org.rekotlin.Action @@ -94,19 +92,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde userCountryCode = action.countryCode, ) } - is GlobalAction.UpdateUserWalletsListManager -> { - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - - if (featureToggles.isGeneralManagerEnabled) { - val generalUserWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - appStateHolder.userWalletsListManager = generalUserWalletsListManager - globalState.copy(userWalletsListManager = generalUserWalletsListManager) - } else { - appStateHolder.userWalletsListManager = action.manager - globalState.copy(userWalletsListManager = action.manager) - } - } is GlobalAction.ChangeAppThemeMode -> globalState.copy( appThemeMode = action.appThemeMode, ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 20712881c8..7a75d16d87 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager @@ -27,7 +26,6 @@ data class GlobalState( val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val userCountryCode: String? = null, - val userWalletsListManager: UserWalletsListManager? = null, val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT, ) : StateType 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 87b5944231..bd50b28faf 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -1,7 +1,7 @@ package com.tangem.tap.data import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.firstOrNull @@ -9,26 +9,24 @@ import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented // [REDACTED_JIRA] internal class RuntimeUserWalletsStore( - private val walletsStateHolder: WalletsStateHolder, + private val userWalletsListManager: UserWalletsListManager, ) : UserWalletsStore { override val selectedUserWalletOrNull: UserWallet? - get() = walletsStateHolder.userWalletsListManager?.selectedUserWalletSync + get() = userWalletsListManager.selectedUserWalletSync override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { - return walletsStateHolder.userWalletsListManager - ?.userWallets - ?.firstOrNull() + return userWalletsListManager + .userWallets + .firstOrNull() ?.singleOrNull { it.walletId == key } } override suspend fun getAllSyncOrNull(): List? { - return walletsStateHolder.userWalletsListManager - ?.userWallets - ?.firstOrNull() + return userWalletsListManager.userWallets.firstOrNull() } override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) { - walletsStateHolder.userWalletsListManager?.update(userWalletId, update) + userWalletsListManager.update(userWalletId, update) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 9a0f838f4a..cbf8eebb27 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,20 +1,20 @@ package com.tangem.tap.di -import android.content.Context import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.feature.tester.ActivityClassWrapper +import com.tangem.tap.MainActivity +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder 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.Dispatchers @@ -25,15 +25,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object ActivityModule { - @Provides - @Singleton - fun provideTangemSdkManager( - @ApplicationContext context: Context, - cardSdkConfigRepository: CardSdkConfigRepository, - ): TangemSdkManager { - return TangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) - } - @Provides @Singleton fun provideScanCardUseCase( @@ -76,4 +67,10 @@ internal object ActivityModule { ): GetPolkadotCheckHasImmortalUseCase { return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository) } + + @Provides + @Singleton + fun provideActivityClassWrapper(): ActivityClassWrapper { + return ActivityClassWrapper(MainActivity::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt index e1bf363dd7..c6e0267c6b 100644 --- a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt @@ -2,7 +2,6 @@ package com.tangem.tap.di import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.proxy.AppStateHolder import dagger.Binds import dagger.Module @@ -14,10 +13,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface AppStateHolderModule { - @Binds - @Singleton - fun bindsWalletsStateHolder(appStateHolder: AppStateHolder): WalletsStateHolder - @Binds @Singleton fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController diff --git a/app/src/main/java/com/tangem/tap/di/ClipboardManagerModule.kt b/app/src/main/java/com/tangem/tap/di/ClipboardManagerModule.kt new file mode 100644 index 0000000000..052f1c4541 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/ClipboardManagerModule.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.di + +import android.content.Context +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.tap.common.clipboard.MockClipboardManager +import com.tangem.tap.common.clipboard.DefaultClipboardManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import android.content.ClipboardManager as AndroidClipboardManager + +@Module +@InstallIn(SingletonComponent::class) +internal class ClipboardManagerModule { + + @Provides + @Singleton + fun provideClipboardManager(@ApplicationContext context: Context): ClipboardManager { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? AndroidClipboardManager + + return if (clipboardManager != null) { + DefaultClipboardManager(clipboardManager = clipboardManager) + } else { + MockClipboardManager + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/HapticModule.kt b/app/src/main/java/com/tangem/tap/di/HapticModule.kt new file mode 100644 index 0000000000..bf8439f09e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/HapticModule.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.di + +import android.content.Context +import android.os.Build +import android.os.Vibrator +import android.os.VibratorManager +import com.tangem.tap.common.haptic.DefaultHapticManager +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.MockHapticManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class HapticModule { + + @Provides + @Singleton + fun provideHapticManager(@ApplicationContext context: Context): HapticManager { + val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + + return if (vibrator.hasVibrator()) { + DefaultHapticManager(vibrator = vibrator) + } else { + MockHapticManager + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt new file mode 100644 index 0000000000..b23a41b8a8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt @@ -0,0 +1,47 @@ +package com.tangem.tap.di + +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import com.arkivanov.decompose.defaultComponentContext +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.DefaultAppComponentContext +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.di.RootAppComponentContext +import com.tangem.core.decompose.ui.UiMessage +import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped +import timber.log.Timber + +@Module +@InstallIn(ActivityComponent::class) +internal object RootAppComponentContextModule { + + @Provides + @ActivityScoped + @RootAppComponentContext + fun provideRootAppComponentContext( + @ActivityContext context: Context, + dispatchers: CoroutineDispatcherProvider, + componentBuilder: DecomposeComponent.Builder, + ): AppComponentContext { + // TODO: Implement message handler + val dummyMessageHandler = object : UiMessageHandler { + override fun handleMessage(message: UiMessage) { + Timber.w("Unable to handle message: $message") + } + } + + return DefaultAppComponentContext( + componentContext = (context as AppCompatActivity).defaultComponentContext(), + messageHandler = dummyMessageHandler, + dispatchers = dispatchers, + hiltComponentBuilder = componentBuilder, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt new file mode 100644 index 0000000000..c6118d1ac1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.di + +import android.content.Context +import com.tangem.domain.card.BuildConfig +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager +import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class TangemSdkManagerModule { + + @Provides + @Singleton + fun provideTangemSdkManager( + @ApplicationContext context: Context, + cardSdkConfigRepository: CardSdkConfigRepository, + ): TangemSdkManager { + return if (BuildConfig.MOCK_DATA_SOURCE) { + MockTangemSdkManager(resources = context.resources) + } else { + DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt index af2732b2d9..e4b2deeb8d 100644 --- a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt @@ -5,13 +5,15 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object ThemeModule { @Provides + @Singleton fun provideAppThemeModeHolder(): AppThemeModeHolder { return MutableAppThemeModeHolder } diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt new file mode 100644 index 0000000000..97b0f48bee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di + +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.theme.AppThemeModeHolder +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 UiDependenciesModule { + + @Provides + @Singleton + fun provideUiDependencies(hapticManager: HapticManager, appThemeModeHolder: AppThemeModeHolder): UiDependencies { + return object : UiDependencies { + override val hapticManager = hapticManager + override val appThemeModeHolder = appThemeModeHolder + } + } +} \ 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 index 0f7457c20f..b87e417fab 100644 --- a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt @@ -2,7 +2,7 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.card.DefaultDerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module 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 6aeaad1dcf..b2a04f9d52 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 @@ -1,7 +1,7 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.data.RuntimeUserWalletsStore import dagger.Module import dagger.Provides @@ -15,7 +15,7 @@ internal object UserWalletsStoreModule { @Provides @Singleton - fun provideUserWalletsStore(walletsStateHolder: WalletsStateHolder): UserWalletsStore { - return RuntimeUserWalletsStore(walletsStateHolder) + fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore { + return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt index f785627aa1..a85859ab25 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt @@ -8,14 +8,15 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object AppCurrencyDomainModule { @Provides + @Singleton fun provideGetSelectedAppCurrencyUseCase( appCurrencyRepository: AppCurrencyRepository, ): GetSelectedAppCurrencyUseCase { @@ -23,11 +24,13 @@ internal object AppCurrencyDomainModule { } @Provides + @Singleton fun provideSelectAppCurrencyUseCase(appCurrencyRepository: AppCurrencyRepository): SelectAppCurrencyUseCase { return SelectAppCurrencyUseCase(appCurrencyRepository) } @Provides + @Singleton fun provideGetAvailableCurrenciesUseCase( appCurrencyRepository: AppCurrencyRepository, ): GetAvailableCurrenciesUseCase { @@ -35,7 +38,7 @@ internal object AppCurrencyDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideFetchAppCurrenciesUseCase(appCurrencyRepository: AppCurrencyRepository): FetchAppCurrenciesUseCase { return FetchAppCurrenciesUseCase(appCurrencyRepository) } 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 7a05bef47d..1a6c5ff684 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 @@ -6,10 +6,11 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase -import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase +import com.tangem.tap.domain.card.DefaultResetCardUseCase +import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -36,14 +37,6 @@ internal object CardDomainModule { return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) } - @Provides - @ViewModelScoped - fun provideGetAccessCodeSavingStatusUseCase( - cardSdkConfigRepository: CardSdkConfigRepository, - ): GetAccessCodeSavingStatusUseCase { - return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) - } - @Provides @ViewModelScoped fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase { @@ -68,8 +61,8 @@ internal object CardDomainModule { @Provides @ViewModelScoped - fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase(walletStateHolder) + fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) } @Provides @@ -85,4 +78,10 @@ internal object CardDomainModule { fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase { return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager) } + + @Provides + @ViewModelScoped + fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase { + return DefaultResetCardUseCase(tangemSdkManager) + } } \ No newline at end of file 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 3cfbe41b23..9654238c85 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 @@ -1,6 +1,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.* +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import dagger.Module import dagger.Provides @@ -14,5 +16,11 @@ internal object CardLegacyDomainModule { @Provides @Singleton - fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() + fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor() + + @Provides + @Singleton + fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase { + return GenerateWalletNameUseCase(userWalletsListManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt index f2aea82f57..033e322536 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -1,7 +1,8 @@ package com.tangem.tap.di.domain import android.content.Context -import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase +import com.tangem.domain.feedback.GetFeedbackEmailUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.repository.FeedbackRepository import dagger.Module import dagger.Provides @@ -16,10 +17,16 @@ internal object FeedbackDomainModule { @Provides @Singleton - fun provideGetFeedbackToSupportUseCase( + fun provideGetFeedbackEmailUseCase( feedbackRepository: FeedbackRepository, @ApplicationContext context: Context, - ): GetSupportFeedbackEmailUseCase { - return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) + ): GetFeedbackEmailUseCase { + return GetFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) + } + + @Provides + @Singleton + fun provideSaveBlockchainErrorUseCase(feedbackRepository: FeedbackRepository): SaveBlockchainErrorUseCase { + return SaveBlockchainErrorUseCase(feedbackRepository = feedbackRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 629af1e441..bac2d55655 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -9,38 +9,38 @@ import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object SettingsDomainModule { @Provides - @ViewModelScoped + @Singleton fun providesIsReadyToShowRatingUseCase(appRatingRepository: AppRatingRepository): IsReadyToShowRateAppUseCase { return IsReadyToShowRateAppUseCase(appRatingRepository = appRatingRepository) } @Provides - @ViewModelScoped + @Singleton fun providesRemindToRateAppLaterUseCase(appRatingRepository: AppRatingRepository): RemindToRateAppLaterUseCase { return RemindToRateAppLaterUseCase(appRatingRepository = appRatingRepository) } @Provides - @ViewModelScoped + @Singleton fun providesNeverToSuggestRateAppUseCase(appRatingRepository: AppRatingRepository): NeverToSuggestRateAppUseCase { return NeverToSuggestRateAppUseCase(appRatingRepository = appRatingRepository) } @Provides - @ViewModelScoped + @Singleton fun providesSetWalletWithFundsFoundUseCase( appRatingRepository: AppRatingRepository, ): SetWalletWithFundsFoundUseCase { @@ -48,7 +48,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun providesShouldShowSaveWalletScreenUseCase( settingsRepository: SettingsRepository, ): ShouldShowSaveWalletScreenUseCase { @@ -56,7 +56,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase { return CanUseBiometryUseCase( legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager), @@ -64,7 +64,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun providesGetBalanceHidingSettingsUseCase( balanceHidingRepository: BalanceHidingRepository, ): GetBalanceHidingSettingsUseCase { @@ -74,7 +74,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun providesListenUseCase( flipDetector: DeviceFlipDetector, balanceHidingRepository: BalanceHidingRepository, @@ -86,7 +86,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideUpdateHideBalancesSettingsUseCase( balanceHidingRepository: BalanceHidingRepository, ): UpdateBalanceHidingSettingsUseCase { @@ -94,19 +94,19 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideSetWalletsScrollPreviewIsShown(settingsRepository: SettingsRepository): NeverToShowWalletsScrollPreview { return NeverToShowWalletsScrollPreview(settingsRepository = settingsRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled { return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) } @Provides - @ViewModelScoped + @Singleton fun provideShouldShowSwapPromoWalletUseCase( promoSettingsRepository: PromoSettingsRepository, ): ShouldShowSwapPromoWalletUseCase { @@ -114,7 +114,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideShouldShowTravalaPromoWalletUseCase( promoSettingsRepository: PromoSettingsRepository, ): ShouldShowTravalaPromoWalletUseCase { @@ -122,7 +122,7 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideShouldShowSwapPromoTokenUseCase( promoSettingsRepository: PromoSettingsRepository, ): ShouldShowSwapPromoTokenUseCase { @@ -130,13 +130,13 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase { return DeleteDeprecatedLogsUseCase(settingsRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsSendTapHelpPreviewEnabledUseCase( settingsRepository: SettingsRepository, ): IsSendTapHelpEnabledUseCase { @@ -144,8 +144,24 @@ internal object SettingsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideNeverShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverShowTapHelpUseCase { return NeverShowTapHelpUseCase(settingsRepository = settingsRepository) } + + @Provides + @Singleton + fun provideSetSaveWalletScreenShownUseCase( + settingsRepository: SettingsRepository, + ): SetSaveWalletScreenShownUseCase { + return SetSaveWalletScreenShownUseCase(settingsRepository = settingsRepository) + } + + @Provides + @Singleton + fun provideIncrementAppLaunchCounterUseCase( + settingsRepository: SettingsRepository, + ): IncrementAppLaunchCounterUseCase { + return IncrementAppLaunchCounterUseCase(settingsRepository = settingsRepository) + } } \ 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 new file mode 100644 index 0000000000..54bfe9d2b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.settings.* +import com.tangem.domain.staking.GetStakingAvailabilityUseCase +import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.repositories.StakingRepository +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 StakingDomainModule { + + @Provides + @Singleton + fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase { + return GetStakingEntryInfoUseCase( + stakingRepository = stakingRepository, + ) + } + + @Provides + @Singleton + fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { + return GetStakingAvailabilityUseCase( + stakingRepository = stakingRepository, + ) + } +} \ 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 662e68f7bb..40ba3c5084 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 @@ -6,21 +6,20 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) @Suppress("TooManyFunctions", "LargeClass") internal object TokensDomainModule { @Provides - @ViewModelScoped + @Singleton fun provideAddCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, networksRepository: NetworksRepository, @@ -32,7 +31,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideFetchTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -42,7 +41,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideFetchPendingTransactionsUseCase( networksRepository: NetworksRepository, ): FetchPendingTransactionsUseCase { @@ -50,29 +49,27 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetTokenListUseCase { - return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository) } @Provides - @ViewModelScoped + @Singleton fun provideGetCardTokensListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetCardTokensListUseCase { - return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository) } @Provides - @ViewModelScoped + @Singleton fun provideRemoveCurrencyUseCase( currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, @@ -81,7 +78,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetCurrencyUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -92,7 +89,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetCurrencyWarningsUseCase( walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, @@ -120,7 +117,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetPrimaryCurrencyUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -136,7 +133,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideFetchCurrencyStatusUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -146,7 +143,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideFetchCardTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -156,13 +153,13 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase { return GetCryptoCurrencyUseCase(currenciesRepository) } @Provides - @ViewModelScoped + @Singleton fun provideToggleTokenListGroupingUseCase( dispatchers: CoroutineDispatcherProvider, ): ToggleTokenListGroupingUseCase { @@ -170,13 +167,13 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { return ToggleTokenListSortingUseCase(dispatchers) } @Provides - @ViewModelScoped + @Singleton fun provideApplyTokenListSortingUseCase( currenciesRepository: CurrenciesRepository, dispatchers: CoroutineDispatcherProvider, @@ -185,29 +182,29 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetCryptoCurrencyActionsUseCase( rampStateManager: RampStateManager, + walletManagersFacade: WalletManagersFacade, marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - sendFeatureToggles: SendFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( rampManager = rampStateManager, + walletManagersFacade = walletManagersFacade, marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - sendFeatureToggles = sendFeatureToggles, dispatchers = dispatchers, ) } @Provides - @ViewModelScoped + @Singleton fun provideGetCurrencyStatusByNetworkUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -223,7 +220,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, @@ -239,13 +236,13 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsCryptoCurrencyCoinCouldHideUseCase( currenciesRepository: CurrenciesRepository, ): IsCryptoCurrencyCoinCouldHideUseCase { @@ -255,7 +252,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideUpdateDelayedCurrencyStatusUseCase( networksRepository: NetworksRepository, ): UpdateDelayedNetworkStatusUseCase { @@ -265,7 +262,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideHasMissedAddressesCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, ): GetMissedAddressesCryptoCurrenciesUseCase { @@ -273,13 +270,13 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase { return GetGlobalTokenListUseCase(repository = tokensListRepository) } @Provides - @ViewModelScoped + @Singleton fun provideCheckTokenCompatibilityUseCase( networksCompatibilityRepository: NetworksCompatibilityRepository, ): CheckCurrencyCompatibilityUseCase { @@ -287,7 +284,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideNeedHardenedDerivationUseCase( networksCompatibilityRepository: NetworksCompatibilityRepository, ): RequiresHardenedDerivationOnlyUseCase { @@ -295,7 +292,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideFindTokenByContractAddressUseCase( tokensListRepository: TokensListRepository, ): FindTokenByContractAddressUseCase { @@ -303,7 +300,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideValidateContractAddressUseCase( tokensListRepository: TokensListRepository, ): ValidateContractAddressUseCase { @@ -311,7 +308,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideAreTokensSupportedByNetworkUseCase( repository: NetworksCompatibilityRepository, ): AreTokensSupportedByNetworkUseCase { @@ -319,7 +316,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetNetworksSupportedByWallet( repository: NetworksCompatibilityRepository, ): GetNetworksSupportedByWallet { @@ -327,7 +324,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetBalanceNotEnoughForFeeWarningUseCase( currenciesRepository: CurrenciesRepository, dispatchers: CoroutineDispatcherProvider, @@ -336,7 +333,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideIsAmountSubtractAvailableUseCase( currenciesRepository: CurrenciesRepository, dispatchers: CoroutineDispatcherProvider, @@ -345,7 +342,7 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideRunPolkadotAccountHealthCheckUseCase( repository: PolkadotAccountHealthCheckRepository, ): RunPolkadotAccountHealthCheckUseCase { @@ -353,10 +350,24 @@ internal object TokensDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideGetNetworkStatusesUseCase(networksRepository: NetworksRepository): GetNetworkAddressesUseCase { return GetNetworkAddressesUseCase( networksRepository = networksRepository, ) } + + @Provides + @Singleton + fun provideGetWalletTotalBalanceUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + ): GetWalletTotalBalanceUseCase { + return GetWalletTotalBalanceUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + } } \ 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 bf4f00059e..9635e67a29 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 @@ -2,25 +2,24 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object TransactionDomainModule { @Provides - @ViewModelScoped + @Singleton fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase { return GetFeeUseCase( walletManagersFacade = walletManagersFacade, @@ -29,7 +28,7 @@ internal object TransactionDomainModule { } @Provides - @ViewModelScoped + @Singleton fun provideSendTransactionUseCase( cardSdkConfigRepository: CardSdkConfigRepository, transactionRepository: TransactionRepository, @@ -44,14 +43,44 @@ internal object TransactionDomainModule { } @Provides - @ViewModelScoped + @Singleton + fun provideAssociateAssetUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, + ): AssociateAssetUseCase { + return AssociateAssetUseCase( + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + ) + } + + @Provides + @Singleton fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { return CreateTransactionUseCase(transactionRepository) } @Provides - @ViewModelScoped + @Singleton fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { return IsFeeApproximateUseCase(feeRepository) } + + @Provides + @Singleton + fun provideValidateTransactionUseCase(transactionRepository: TransactionRepository): ValidateTransactionUseCase { + return ValidateTransactionUseCase(transactionRepository) + } + + @Provides + @Singleton + fun provideIsUtxoConsolidationAvailableUseCase( + walletManagersFacade: WalletManagersFacade, + ): IsUtxoConsolidationAvailableUseCase { + return IsUtxoConsolidationAvailableUseCase(walletManagersFacade) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt new file mode 100644 index 0000000000..e542ddc5fd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase +import com.tangem.domain.walletconnect.repository.WalletConnectRepository +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 WalletConnectDomainModule { + + @Provides + @Singleton + fun providesCheckIsWalletConnectAvailableUseCase( + walletConnectRepository: WalletConnectRepository, + ): CheckIsWalletConnectAvailableUseCase { + return CheckIsWalletConnectAvailableUseCase(walletConnectRepository = walletConnectRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt index 319ddd2c0e..a54c6ecc0c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -1,18 +1,12 @@ package com.tangem.tap.di.domain -import com.squareup.moshi.Moshi -import com.tangem.blockchain.common.AccountCreator -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.datasource.asset.AssetReader -import com.tangem.datasource.config.ConfigManager -import com.tangem.datasource.di.SdkMoshi +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.walletmanager.DefaultWalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.feature.onboarding.data.MnemonicRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -28,26 +22,16 @@ internal object WalletManagersFacadeModule { fun provideWalletManagersFacade( walletManagersStore: WalletManagersStore, userWalletsStore: UserWalletsStore, - configManager: ConfigManager, - blockchainDataStorage: BlockchainDataStorage, - accountCreator: AccountCreator, - mnemonicRepository: MnemonicRepository, - assetReader: AssetReader, - @SdkMoshi moshi: Moshi, - blockchainSDKLogger: BlockchainSDKLogger, - feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, + assetLoader: AssetLoader, + blockchainSDKFactory: BlockchainSDKFactory, + dispatchers: CoroutineDispatcherProvider, ): WalletManagersFacade { return DefaultWalletManagersFacade( walletManagersStore = walletManagersStore, userWalletsStore = userWalletsStore, - configManager = configManager, - blockchainDataStorage = blockchainDataStorage, - assetReader = assetReader, - moshi = moshi, - mnemonic = mnemonicRepository.generateDefaultMnemonic(), - accountCreator = accountCreator, - blockchainSDKLogger = blockchainSDKLogger, - feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + assetLoader = assetLoader, + dispatchers = dispatchers, + blockchainSDKFactory = blockchainSDKFactory, ) } } \ 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 c7f58a8226..3dfc085c10 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 @@ -2,86 +2,117 @@ package com.tangem.tap.di.domain import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.repository.WalletAddressServiceRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +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.WalletNameMigrationUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object WalletsDomainModule { @Provides - @ViewModelScoped - fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { - return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { + return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped - fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase { - return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesWalletNameMigrationUseCase( + userWalletsListManager: UserWalletsListManager, + walletNamesMigrationRepository: WalletNamesMigrationRepository, + ): WalletNameMigrationUseCase { + return WalletNameMigrationUseCase( + userWalletsListManager = userWalletsListManager, + walletNamesMigrationRepository = walletNamesMigrationRepository, + ) } @Provides - @ViewModelScoped - fun providesGetSelectedWalletSyncUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { + return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped - fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesGetSelectedWalletSyncUseCase( + userWalletsListManager: UserWalletsListManager, + ): GetSelectedWalletSyncUseCase { + return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped - fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { - return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped + @Singleton + fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { + return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) + } + + @Provides + @Singleton fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) } @Provides - @ViewModelScoped - fun providesUnlockWalletUseCase(walletsStateHolder: WalletsStateHolder): UnlockWalletsUseCase { - return UnlockWalletsUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesUnlockWalletUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase { + return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped + @Singleton fun providesSelectWalletUseCase( - walletsStateHolder: WalletsStateHolder, + userWalletsListManager: UserWalletsListManager, reduxStateHolder: ReduxStateHolder, ): SelectWalletUseCase { - return SelectWalletUseCase(walletsStateHolder, reduxStateHolder) + return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder) } @Provides - @ViewModelScoped - fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase { - return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { + return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped - fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase { - return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) + @Singleton + fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase { + return RenameWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides - @ViewModelScoped + @Singleton + fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase { + return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager) + } + + @Provides + @Singleton + fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { + return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) + } + + @Provides + @Singleton fun providesShouldSaveUserWalletsSyncUseCase( walletsRepository: WalletsRepository, ): ShouldSaveUserWalletsSyncUseCase { @@ -89,25 +120,25 @@ internal object WalletsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase { return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository) } @Provides - @ViewModelScoped + @Singleton fun providesValidateWalletAddressUseCase( walletAddressServiceRepository: WalletAddressServiceRepository, - dispatchers: CoroutineDispatcherProvider, + walletManagersFacade: WalletManagersFacade, ): ValidateWalletAddressUseCase { return ValidateWalletAddressUseCase( walletAddressServiceRepository = walletAddressServiceRepository, - dispatchers = dispatchers, + walletManagersFacade = walletManagersFacade, ) } @Provides - @ViewModelScoped + @Singleton fun providesValidateWalletMemoUseCase( walletAddressServiceRepository: WalletAddressServiceRepository, ): ValidateWalletMemoUseCase { @@ -115,7 +146,7 @@ internal object WalletsDomainModule { } @Provides - @ViewModelScoped + @Singleton fun providesParseSharedAddressUseCase( walletAddressServiceRepository: WalletAddressServiceRepository, dispatchers: CoroutineDispatcherProvider, diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index f8a93381e8..256c8f1bf9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,9 +1,7 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic import com.tangem.datasource.config.ConfigManager @@ -15,7 +13,6 @@ import com.tangem.operations.attestation.Attestation import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -32,29 +29,12 @@ class TapWalletManager( private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(), ) { - private val blockchainSdkConfig by lazy { - store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() - } - private var loadUserWalletDataJob: Job? = null set(value) { field?.cancel() field = value } - val walletManagerFactory: WalletManagerFactory by lazy { - WalletManagerFactory( - config = blockchainSdkConfig, - accountCreator = store.inject(DaggerGraphState::accountCreator), - blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage), - loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { - listOf(store.inject(DaggerGraphState::blockchainSDKLogger)) - } else { - emptyList() - }, - ) - } - suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) { // If a previous job was running, it gets cancelled before the new one starts, // ensuring that only one job is active at any given time. @@ -73,15 +53,18 @@ class TapWalletManager( val attestationFailed = card.attestation.status == Attestation.Status.Failed tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) - store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse) + + val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) + if (!featureToggles.isLocalLogsEnabled) { + store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse) + } + updateConfigManager(scanResponse) withMainContext { // Order is important store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer())) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - store.dispatch(WalletConnectAction.ResetState) store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) - store.dispatch(WalletConnectAction.RestoreSessions(scanResponse)) store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) } } diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt index b0fadb5bcf..8834604935 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.card.DeleteSavedAccessCodesUseCase -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager internal class DefaultDeleteSavedAccessCodesUseCase( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 84d55370ed..b7ad02d630 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -9,11 +9,11 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import timber.log.Timber diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt new file mode 100644 index 0000000000..244dd02b7b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.domain.card + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.tangem.common.CompletionResult +import com.tangem.common.UserCodeType +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.doOnResult +import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.models.ResetCardError +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.tap.domain.sdk.TangemSdkManager + +internal class DefaultResetCardUseCase( + private val tangemSdkManager: TangemSdkManager, +) : ResetCardUseCase { + + override suspend fun invoke(card: CardDTO): Either = either { + enterRequiredAccessCode(card) { + resetToFactorySettings( + cardId = card.cardId, + allowsRequestAccessCodeFromRepository = true, + ) + } + .mapToEither() + } + + override suspend fun invoke( + cardNumber: Int, + card: CardDTO, + userWalletId: UserWalletId, + ): Either = either { + enterRequiredAccessCode(card) { + resetBackupCard(cardNumber = cardNumber, userWalletId = userWalletId) + } + .mapToEither() + } + + private suspend fun enterRequiredAccessCode( + card: CardDTO, + task: suspend TangemSdkManager.() -> CompletionResult<*>, + ): CompletionResult<*> { + val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy + requestMandatoryAccessCodeEntry(card) + + return tangemSdkManager.task() + .doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) } + } + + private fun requestMandatoryAccessCodeEntry(card: CardDTO) { + val type = if (card.isAccessCodeSet) { + UserCodeType.AccessCode + } else if (card.isPasscodeSet == true) { + UserCodeType.Passcode + } else { + null + } + + type?.let { + tangemSdkManager.setUserCodeRequestPolicy(policy = UserCodeRequestPolicy.Always(type)) + } + } + + private fun CompletionResult<*>.mapToEither(): Either { + return when (this) { + is CompletionResult.Failure -> error.mapToDomainError().left() + is CompletionResult.Success -> Unit.right() + } + } + + private fun TangemError.mapToDomainError(): ResetCardError { + return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.AnotherSdkError + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index c71548c0bd..9497f54791 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -5,7 +5,7 @@ import com.tangem.common.services.Result import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.userwallets.Artwork +import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier @@ -20,8 +20,8 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result Artwork.MARTA_CARD_URL else -> { when (getTwinCardNumber()) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1 - TwinCardNumber.Second -> Artwork.TWIN_CARD_2 + TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL + TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL else -> Artwork.DEFAULT_IMG_URL } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index 8aeacbb2c9..ee417278e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard.repository import com.tangem.common.CompletionResult import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter // TODO: Move to the :data:card module diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt new file mode 100644 index 0000000000..e3de1dc4a7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.domain.sdk + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.tangem.Message +import com.tangem.common.CompletionResult +import com.tangem.common.KeyPair +import com.tangem.common.SuccessResponse +import com.tangem.common.authentication.keystore.KeystoreManager +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.secure.SecureStorage +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.wallets.models.UserWalletId +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +@Suppress("TooManyFunctions") +interface TangemSdkManager { + + val canUseBiometry: Boolean + + val needEnrollBiometrics: Boolean + + val keystoreManager: KeystoreManager + + val secureStorage: SecureStorage + + val userCodeRequestPolicy: UserCodeRequestPolicy + + suspend fun scanProduct( + cardId: String? = null, + messageRes: Int? = null, + allowsRequestAccessCodeFromRepository: Boolean = false, + ): CompletionResult + + suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean = false, + ): CompletionResult + + // Wallet2 specific + suspend fun importWallet( + scanResponse: ScanResponse, + mnemonic: String, + passphrase: String?, + shouldReset: Boolean, + ): CompletionResult + + suspend fun derivePublicKeys( + cardId: String?, + derivations: Map>, + ): CompletionResult + + suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult + + suspend fun resetToFactorySettings( + cardId: String, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult + + suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult + + suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult + + suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult + + suspend fun clearSavedUserCodes(): CompletionResult + + suspend fun setPasscode(cardId: String?): CompletionResult + + suspend fun setAccessCode(cardId: String?): CompletionResult + + suspend fun setLongTap(cardId: String?): CompletionResult + + suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult + + suspend fun scanCard( + cardId: String? = null, + allowRequestAccessCodeFromRepository: Boolean = false, + ): CompletionResult + + @Deprecated( + "TangemSdkManager shouldn't run custom tasks. " + + "All of them should be specified in TangemSdkManager certain methods.", + ) + suspend fun runTaskAsync( + runnable: CardSessionRunnable, + cardId: String? = null, + initialMessage: Message? = null, + accessCode: String? = null, + @DrawableRes iconScanRes: Int? = null, + ): CompletionResult + + @Suppress("MagicNumber") + fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) + + @Deprecated("TangemSdkManager shouldn't returns a string from resources") + fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String + + fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) + + // region Twin-specific + + suspend fun finalizeTwin( + secondCardPublicKey: ByteArray, + issuerKeyPair: KeyPair, + cardId: String, + initialMessage: Message, + ): CompletionResult + + suspend fun createFirstTwinWallet(cardId: String, initialMessage: Message): CompletionResult + + @Suppress("LongParameterList") + suspend fun createSecondTwinWallet( + firstPublicKey: String, + firstCardId: String, + issuerKeys: KeyPair, + preparingMessage: Message, + creatingWalletMessage: Message, + initialMessage: Message, + ): CompletionResult + + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt similarity index 68% rename from app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt rename to app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 4097061289..72b578cea9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain +package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes @@ -16,23 +16,26 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.tasks.product.CreateProductWalletTask -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse -import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask -import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.domain.tasks.product.* +import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask +import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask +import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine @@ -40,10 +43,10 @@ import kotlinx.coroutines.withContext import kotlin.coroutines.resume @Suppress("TooManyFunctions") -class TangemSdkManager( +class DefaultTangemSdkManager( private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources, -) { +) : TangemSdkManager { private val tangemSdk: TangemSdk get() = cardSdkConfigRepository.sdk @@ -55,25 +58,25 @@ class TangemSdkManager( ) } - val canUseBiometry: Boolean + override val canUseBiometry: Boolean get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics - val needEnrollBiometrics: Boolean + override val needEnrollBiometrics: Boolean get() = tangemSdk.authenticationManager.needEnrollBiometrics - val keystoreManager: KeystoreManager + override val keystoreManager: KeystoreManager get() = tangemSdk.keystoreManager - val secureStorage: SecureStorage + override val secureStorage: SecureStorage get() = tangemSdk.secureStorage - val userCodeRequestPolicy: UserCodeRequestPolicy + override val userCodeRequestPolicy: UserCodeRequestPolicy get() = tangemSdk.config.userCodeRequestPolicy - suspend fun scanProduct( - cardId: String? = null, - messageRes: Int? = null, - allowsRequestAccessCodeFromRepository: Boolean = false, + override suspend fun scanProduct( + cardId: String?, + messageRes: Int?, + allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( @@ -87,9 +90,9 @@ class TangemSdkManager( ).also { sendScanResultsToAnalytics(it) } } - suspend fun createProductWallet( + override suspend fun createProductWallet( scanResponse: ScanResponse, - shouldReset: Boolean = false, + shouldReset: Boolean, ): CompletionResult { return runTaskAsync( runnable = CreateProductWalletTask( @@ -103,7 +106,7 @@ class TangemSdkManager( ) } - suspend fun importWallet( + override suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, passphrase: String?, @@ -135,14 +138,14 @@ class TangemSdkManager( } } - suspend fun derivePublicKeys( + override suspend fun derivePublicKeys( cardId: String?, derivations: Map>, ): CompletionResult { return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } - suspend fun deriveExtendedPublicKey( + override suspend fun deriveExtendedPublicKey( cardId: String?, walletPublicKey: ByteArray, derivation: DerivationPath, @@ -153,7 +156,7 @@ class TangemSdkManager( ) } - suspend fun resetToFactorySettings( + override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { @@ -167,7 +170,19 @@ class TangemSdkManager( .map { CardDTO(it) } } - suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { + return runTaskAsyncReturnOnMain( + runnable = ResetBackupCardTask(userWalletId), + initialMessage = Message( + resources.getString( + R.string.initial_message_reset_backup_card_header, + cardNumber.toString(), + ), + ), + ) + } + + override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { return userCodeRepository.save( cardsIds = cardsIds, userCode = UserCode( @@ -177,15 +192,15 @@ class TangemSdkManager( ) } - suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { + override suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { return userCodeRepository.delete(cardsIds.toSet()) } - suspend fun clearSavedUserCodes(): CompletionResult { + override suspend fun clearSavedUserCodes(): CompletionResult { return userCodeRepository.clear() } - suspend fun setPasscode(cardId: String?): CompletionResult { + override suspend fun setPasscode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, @@ -193,7 +208,7 @@ class TangemSdkManager( ) } - suspend fun setAccessCode(cardId: String?): CompletionResult { + override suspend fun setAccessCode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, @@ -201,7 +216,7 @@ class TangemSdkManager( ) } - suspend fun setLongTap(cardId: String?): CompletionResult { + override suspend fun setLongTap(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, @@ -209,7 +224,10 @@ class TangemSdkManager( ) } - suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult { + override suspend fun setAccessCodeRecoveryEnabled( + cardId: String?, + enabled: Boolean, + ): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeRecoveryAllowedTask(enabled), cardId, @@ -217,9 +235,9 @@ class TangemSdkManager( ) } - suspend fun scanCard( - cardId: String? = null, - allowRequestAccessCodeFromRepository: Boolean = false, + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromRepository: Boolean, ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = ScanTask(allowRequestAccessCodeFromRepository), @@ -229,12 +247,12 @@ class TangemSdkManager( .map { CardDTO(it) } } - suspend fun runTaskAsync( + override suspend fun runTaskAsync( runnable: CardSessionRunnable, - cardId: String? = null, - initialMessage: Message? = null, - accessCode: String? = null, - @DrawableRes iconScanRes: Int? = null, + cardId: String?, + initialMessage: Message?, + accessCode: String?, + @DrawableRes iconScanRes: Int?, ): CompletionResult = withContext(Dispatchers.Main) { suspendCancellableCoroutine { continuation -> tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode, iconScanRes) { result -> @@ -253,7 +271,7 @@ class TangemSdkManager( } @Suppress("MagicNumber") - fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { + override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { tangemSdk.config.cardIdDisplayFormat = when { scanResponse == null -> CardIdDisplayFormat.Full scanResponse.cardTypesResolver.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4) @@ -262,14 +280,60 @@ class TangemSdkManager( } @Deprecated("TangemSdkManager shouldn't returns a string from resources") - fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { + override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { return resources.getString(stringResId, *formatArgs) } - fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { + override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { tangemSdk.config.userCodeRequestPolicy = policy } + // region Twin-specific + + override suspend fun createFirstTwinWallet( + cardId: String, + initialMessage: Message, + ): CompletionResult { + return runTaskAsync( + runnable = CreateFirstTwinWalletTask(cardId), + cardId = cardId, + initialMessage = initialMessage, + ) + } + + override suspend fun createSecondTwinWallet( + firstPublicKey: String, + firstCardId: String, + issuerKeys: KeyPair, + preparingMessage: Message, + creatingWalletMessage: Message, + initialMessage: Message, + ): CompletionResult { + val task = CreateSecondTwinWalletTask( + firstPublicKey = firstPublicKey, + firstCardId = firstCardId, + issuerKeys = issuerKeys, + preparingMessage = preparingMessage, + creatingWalletMessage = creatingWalletMessage, + ) + return runTaskAsync(task, null, initialMessage) + } + + override suspend fun finalizeTwin( + secondCardPublicKey: ByteArray, + issuerKeyPair: KeyPair, + cardId: String, + initialMessage: Message, + ): CompletionResult { + return runTaskAsync( + runnable = FinalizeTwinTask(secondCardPublicKey, issuerKeyPair), + cardId = cardId, + initialMessage = initialMessage, + ) + } + + // endregion + companion object { @Deprecated("Use [DefaultCardSdkProvider] instead") val config = Config( 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 new file mode 100644 index 0000000000..7c5585c68b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -0,0 +1,183 @@ +package com.tangem.tap.domain.sdk.impl + +import android.content.res.Resources +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.tangem.Message +import com.tangem.common.CompletionResult +import com.tangem.common.KeyPair +import com.tangem.common.SuccessResponse +import com.tangem.common.authentication.keystore.DummyKeystoreManager +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.InMemoryStorage +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.wallets.models.UserWalletId +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.domain.sdk.mocks.MockProvider +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +@Suppress("TooManyFunctions") +class MockTangemSdkManager( + private val resources: Resources, +) : TangemSdkManager { + + override val canUseBiometry = false + + override val needEnrollBiometrics = false + + override val keystoreManager = DummyKeystoreManager() + + override val secureStorage = InMemoryStorage() + + override val userCodeRequestPolicy: UserCodeRequestPolicy + get() = userCodeRequestPolicyInternal + + private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default + + override suspend fun scanProduct( + cardId: String?, + messageRes: Int?, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return MockProvider.getScanResponse() + } + + override suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean, + ): CompletionResult { + return MockProvider.getCreateProductWalletResponse() + } + + override suspend fun importWallet( + scanResponse: ScanResponse, + mnemonic: String, + passphrase: String?, + shouldReset: Boolean, + ): CompletionResult { + return MockProvider.getImportWalletResponse() + } + + override suspend fun derivePublicKeys( + cardId: String?, + derivations: Map>, + ): CompletionResult { + return MockProvider.getDerivationTaskResponse() + } + + override suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult { + return MockProvider.getExtendedPublicKey() + } + + override suspend fun resetToFactorySettings( + cardId: String, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return MockProvider.getCardDto() + } + + override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun clearSavedUserCodes(): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun setPasscode(cardId: String?): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun setAccessCode(cardId: String?): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun setLongTap(cardId: String?): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun setAccessCodeRecoveryEnabled( + cardId: String?, + enabled: Boolean, + ): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return MockProvider.getCardDto() + } + + override suspend fun runTaskAsync( + runnable: CardSessionRunnable, + cardId: String?, + initialMessage: Message?, + accessCode: String?, + @DrawableRes iconScanRes: Int?, + ): CompletionResult = error("This method is deprecated") + + override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { + // intentionally do nothing + } + + @Deprecated("TangemSdkManager shouldn't returns a string from resources") + override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { + return resources.getString(stringResId, *formatArgs) + } + + override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { + userCodeRequestPolicyInternal = policy + } + + // region Twin-specific + + override suspend fun createFirstTwinWallet( + cardId: String, + initialMessage: Message, + ): CompletionResult { + return MockProvider.createFirstTwinWallet() + } + + override suspend fun createSecondTwinWallet( + firstPublicKey: String, + firstCardId: String, + issuerKeys: KeyPair, + preparingMessage: Message, + creatingWalletMessage: Message, + initialMessage: Message, + ): CompletionResult { + return MockProvider.createSecondTwinWallet() + } + + override suspend fun finalizeTwin( + secondCardPublicKey: ByteArray, + issuerKeyPair: KeyPair, + cardId: String, + initialMessage: Message, + ): CompletionResult { + return MockProvider.finalizeTwin() + } + + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt new file mode 100644 index 0000000000..bb8ca7deab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.domain.sdk.mocks + +import com.tangem.common.SuccessResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +interface MockContent { + + val successResponse: SuccessResponse + + val scanResponse: ScanResponse + + val derivationTaskResponse: DerivationTaskResponse + + val cardDto: CardDTO + + val extendedPublicKey: ExtendedPublicKey + + val createProductWalletTaskResponse: CreateProductWalletTaskResponse + + // Wallet2-specific + val importWalletResponse: CreateProductWalletTaskResponse + + // Twin-specific + val finalizeTwinResponse: ScanResponse + + // Twin-specific + val createFirstTwinResponse: CreateWalletResponse + + // Twin-specific + val createSecondTwinResponse: CreateWalletResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt new file mode 100644 index 0000000000..9468912e1c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -0,0 +1,81 @@ +package com.tangem.tap.domain.sdk.mocks + +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.scan.ProductType +import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent +import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +object MockProvider { + + private var content: MockContent = getMockContent(ProductType.Wallet) + + private var emulateError: Boolean = false + + private var emulatedError: TangemError = TangemSdkError.TagLost() + + fun setEmulateError(error: TangemError? = null) { + emulateError = true + error?.let { + emulatedError = it + } + } + + fun resetEmulateError() { + emulateError = false + } + + fun setMocks(productType: ProductType) { + content = getMockContent(productType) + } + + fun setMocks(mockContent: MockContent) { + content = mockContent + } + + fun getSuccessResponse() = CompletionResult.Success(content.successResponse).orFailure() + + fun getScanResponse() = CompletionResult.Success(content.scanResponse).orFailure() + + fun getDerivationTaskResponse() = CompletionResult.Success(content.derivationTaskResponse).orFailure() + + fun getCardDto() = CompletionResult.Success(content.cardDto).orFailure() + + fun getExtendedPublicKey() = CompletionResult.Success(content.extendedPublicKey).orFailure() + + fun getCreateProductWalletResponse(): CompletionResult { + return CompletionResult.Success(content.createProductWalletTaskResponse).orFailure() + } + + fun getImportWalletResponse(): CompletionResult { + return CompletionResult.Success(content.importWalletResponse).orFailure() + } + + // region Twin-specific + + fun finalizeTwin() = CompletionResult.Success(content.finalizeTwinResponse).orFailure() + + fun createFirstTwinWallet() = CompletionResult.Success(content.createFirstTwinResponse).orFailure() + + fun createSecondTwinWallet() = CompletionResult.Success(content.createSecondTwinResponse).orFailure() + + // endregion + + private fun getMockContent(productType: ProductType): MockContent { + return when (productType) { + ProductType.Wallet -> WalletMockContent + ProductType.Wallet2 -> Wallet2MockContent + else -> TODO() + } + } + + private fun CompletionResult.Success.orFailure(): CompletionResult { + return if (emulateError) { + CompletionResult.Failure(emulatedError) + } else { + this + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt new file mode 100644 index 0000000000..2b9a29b938 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +object Wallet2MockContent : MockContent { + + override val scanResponse: ScanResponse + get() = TODO("Not yet implemented") + + override val cardDto: CardDTO + get() = TODO("Not yet implemented") + + override val derivationTaskResponse: DerivationTaskResponse + get() = TODO("Not yet implemented") + + override val extendedPublicKey: ExtendedPublicKey + get() = TODO("Not yet implemented") + + override val successResponse: SuccessResponse + get() = TODO("Not yet implemented") + + override val createProductWalletTaskResponse: CreateProductWalletTaskResponse + get() = TODO("Not yet implemented") + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = TODO("Not yet implemented") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt new file mode 100644 index 0000000000..8e059f5450 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -0,0 +1,247 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import java.util.Date + +object WalletMockContent : MockContent { + + private val primaryCard = PrimaryCard( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + linkingKey = byteArrayOf( + 2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121, + -57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91, + ), + existingWalletsCount = 3, isHDWalletAllowed = true, + issuer = Card.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug), + firmwareVersion = FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + isKeysImportAllowed = false, + certificate = null, + ) + + override val cardDto = CardDTO( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + firmwareVersion = CardDTO.FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 20, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = true, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = true, + isHDWalletAllowed = true, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Secp256r1, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + ), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 0, + hasBackup = false, + derivedKeys = mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + ), + DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), + ), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + curve = EllipticCurve.Ed25519, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 1, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + ), + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet, + walletData = null, + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse( + entries = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch + publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121), + chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge + publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124), + chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + ) + + override val extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110), + chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ) + + override val successResponse = SuccessResponse(cardId = "AC05000000086747") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + primaryCard = primaryCard, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt index 681d4ffabd..00f7f53887 100644 --- a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.settings import com.tangem.domain.settings.repositories.LegacySettingsRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager internal class DefaultLegacySettingsRepository( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt b/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt new file mode 100644 index 0000000000..b0d2d24119 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.domain.tasks + +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.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWalletId +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() + + if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound() + } +} \ No newline at end of file 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 d229d8f05d..d1f7b8a3d6 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 @@ -3,13 +3,13 @@ package com.tangem.tap.domain.tasks.product import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.local.token.UserTokensStore import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.features.demo.DemoHelper import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt new file mode 100644 index 0000000000..7de7ceb374 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -0,0 +1,46 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.common.CompletionResult +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.CompletionCallback +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.operations.PreflightReadMode +import com.tangem.operations.PreflightReadTask +import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter + +/** + * Task for resetting backup card. + * + * 1. Read card and check if card is corresponding to expected user wallet id. + * 2. Reset card. + * +[REDACTED_AUTHOR] + */ +internal class ResetBackupCardTask( + private val userWalletId: UserWalletId, +) : CardSessionRunnable { + + override val allowsRequestAccessCodeFromRepository: Boolean = false + + override fun run(session: CardSession, callback: CompletionCallback) { + PreflightReadTask( + readMode = PreflightReadMode.FullCardRead, + filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), + ).run(session) { result -> + when (result) { + is CompletionResult.Success -> resetCard(session, callback) + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private fun resetCard(session: CardSession, callback: CompletionCallback) { + ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository).run(session) { result -> + when (result) { + is CompletionResult.Success -> callback(CompletionResult.Success(Unit)) + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 6ef5f2bbe9..7f242bb845 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -1,41 +1,36 @@ package com.tangem.tap.domain.twins -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Types import com.tangem.Message import com.tangem.blockchain.extensions.Result import com.tangem.common.CompletionResult import com.tangem.common.KeyPair import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.asset.AssetReader import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.common.extensions.inject +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.flow.MutableStateFlow -class TwinCardsManager( - card: CardDTO, - assetReader: AssetReader, -) { +class TwinCardsManager(card: CardDTO) { private val firstCardId: String = card.cardId + private val publicKey: String = card.issuer.publicKey.toHexString() private var currentCardPublicKey: String? = null private var secondCardPublicKey: String? = null - private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString()) + private val issuerKeyPairFlow = MutableStateFlow(value = null) suspend fun createFirstWallet(message: Message): CompletionResult { - val response = tangemSdkManager.runTaskAsync( - runnable = CreateFirstTwinWalletTask(firstCardId), - cardId = firstCardId, - initialMessage = message, - ) - when (response) { - is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString() - is CompletionResult.Failure -> {} + val response = tangemSdkManager.createFirstTwinWallet(cardId = firstCardId, initialMessage = message) + + if (response is CompletionResult.Success) { + currentCardPublicKey = response.data.wallet.publicKey.toHexString() } + return response } @@ -44,58 +39,54 @@ class TwinCardsManager( preparingMessage: Message, creatingWalletMessage: Message, ): CompletionResult { - val task = CreateSecondTwinWalletTask( + val response = tangemSdkManager.createSecondTwinWallet( firstPublicKey = currentCardPublicKey!!, firstCardId = firstCardId, - issuerKeys = issuerKeyPair, + issuerKeys = getIssuerKeys(), preparingMessage = preparingMessage, creatingWalletMessage = creatingWalletMessage, + initialMessage = initialMessage, ) - val response = tangemSdkManager.runTaskAsync(task, null, initialMessage) - when (response) { - is CompletionResult.Success -> { - secondCardPublicKey = response.data.wallet.publicKey.toHexString() - } - is CompletionResult.Failure -> {} + + if (response is CompletionResult.Success) { + secondCardPublicKey = response.data.wallet.publicKey.toHexString() } + return response } suspend fun complete(message: Message): Result { - val response = tangemSdkManager.runTaskAsync( - runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair), + val response = tangemSdkManager.finalizeTwin( + secondCardPublicKey = secondCardPublicKey!!.hexToBytes(), + issuerKeyPair = getIssuerKeys(), cardId = firstCardId, initialMessage = message, ) + return when (response) { is CompletionResult.Success -> Result.Success(response.data) is CompletionResult.Failure -> Result.fromTangemSdkError(response.error) } } - companion object { - private fun getIssuerKeys(reader: AssetReader, publicKey: String): KeyPair { - val issuer = getIssuers(reader).first { it.publicKey == publicKey } - return KeyPair( - publicKey = issuer.publicKey.hexToBytes(), - privateKey = issuer.privateKey.hexToBytes(), - ) - } + private suspend fun getIssuerKeys(): KeyPair { + issuerKeyPairFlow.value?.let { return it } - private fun getAdapter(): JsonAdapter> { - return MoshiConverter.sdkMoshi.adapter( - Types.newParameterizedType(List::class.java, Issuer::class.java), - ) - } + val assetLoader = store.inject(DaggerGraphState::assetLoader) + val issuer = assetLoader.loadList(fileName = ISSUERS_FILE_NAME) + .first { it.publicKey == publicKey } - private fun getIssuers(reader: AssetReader): List { - val file = reader.readJson(fileName = "tangem-app-config/issuers") - return getAdapter().fromJson(file)!! + return KeyPair( + publicKey = issuer.publicKey.hexToBytes(), + privateKey = issuer.privateKey.hexToBytes(), + ).also { + issuerKeyPairFlow.value = it } } + + private companion object { + const val ISSUERS_FILE_NAME = "tangem-app-config/issuers" + } } -private class Issuer( - val privateKey: String, - val publicKey: String, -) \ No newline at end of file +private class Issuer(val privateKey: String, val publicKey: String) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt deleted file mode 100644 index ca0f135159..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.domain.userWalletList - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles - -internal class DefaultUserWalletsListManagerFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : UserWalletsListManagerFeatureToggles { - - override val isGeneralManagerEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt deleted file mode 100644 index f94041dc73..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.domain.userWalletList.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles -import com.tangem.tap.domain.userWalletList.DefaultUserWalletsListManagerFeatureToggles -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 UserWalletsListManagerFeatureTogglesModule { - - @Provides - @Singleton - fun provideUserWalletsListManagerFeatureToggles( - featureTogglesManager: FeatureTogglesManager, - ): UserWalletsListManagerFeatureToggles { - return DefaultUserWalletsListManagerFeatureToggles(featureTogglesManager) - } -} \ 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 f2e03181e1..1128c659f5 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 @@ -66,7 +66,7 @@ internal object UserWalletsListManagerModule { val secureStorage = AndroidSecureStorage( preferences = SecureStorage.createEncryptedSharedPreferences( context = applicationContext, - storageName = USER_WALLETS_STORAGE_NAME, + storageName = "user_wallets_storage", ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt deleted file mode 100644 index 51bdd56ae6..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.tap.domain.userWalletList.di - -import android.content.Context -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.tangem.common.authentication.storage.AuthenticatedStorage -import com.tangem.common.json.TangemSdkAdapter -import com.tangem.common.services.secure.SecureStorage -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.sdk.storage.AndroidSecureStorage -import com.tangem.sdk.storage.createEncryptedSharedPreferences -import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager -import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager -import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager -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 -import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository -import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository -import com.tangem.tap.domain.userWalletList.utils.json.* -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.Provider - -internal const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" - -fun UserWalletsListManager.Companion.provideBiometricImplementation( - applicationContext: Context, -): 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(KotlinJsonAdapterFactory()) - .build() - - val secureStorage = AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = USER_WALLETS_STORAGE_NAME, - ), - ) - - val authenticatedStorage = AuthenticatedStorage( - secureStorage = UserWalletsKeysStoreDecorator( - featureStorage = secureStorage, - cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, - ), - keystoreManager = DelegatedKeystoreManager( - keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, - ), - ) - - val keysRepository = BiometricUserWalletsKeysRepository( - moshi = moshi, - secureStorage = secureStorage, - authenticatedStorage = authenticatedStorage, - ) - val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( - moshi = moshi, - secureStorage = secureStorage, - ) - val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository( - moshi = moshi, - secureStorage = secureStorage, - ) - val selectedUserWalletRepository = DefaultSelectedUserWalletRepository( - secureStorage = secureStorage, - ) - - return BiometricUserWalletsListManager( - keysRepository = keysRepository, - publicInformationRepository = publicInformationRepository, - sensitiveInformationRepository = sensitiveInformationRepository, - selectedUserWalletRepository = selectedUserWalletRepository, - ) -} - -fun UserWalletsListManager.Companion.provideRuntimeImplementation(): UserWalletsListManager { - return RuntimeUserWalletsListManager() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 99418a7aa0..52e9be2c6d 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -12,6 +12,7 @@ import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.utils.encryptionKey +import com.tangem.tap.domain.userWalletList.utils.lockAll import com.tangem.tap.domain.userWalletList.utils.toUserWallets import com.tangem.tap.domain.userWalletList.utils.updateWith import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -27,11 +28,16 @@ internal class BiometricUserWalletsListManager( ) : UserWalletsListManager.Lockable { private val state = MutableStateFlow(State()) + override val isLockable: Boolean = true + override val userWallets: Flow> get() = state .mapLatest { it.userWallets } .distinctUntilChanged() + override val userWalletsSync: List + get() = state.value.userWallets + override val selectedUserWallet: Flow get() = state .mapLatest { state -> @@ -78,11 +84,13 @@ internal class BiometricUserWalletsListManager( } override fun lock() { - state.update { State() } - } - - override fun isLockable(): Boolean { - return true + state.update { prevState -> + prevState.copy( + encryptionKeys = emptyList(), + userWallets = prevState.userWallets.lockAll(), + isLocked = true, + ) + } } override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 762ba82f54..4565c111ed 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -33,31 +33,52 @@ internal class GeneralUserWalletsListManager( ) : UserWalletsListManager.Lockable { private val applicationScope = CoroutineScope(dispatchers.io) - private val implementation = MutableStateFlow(runtimeUserWalletsListManager) + private val implementation: MutableStateFlow = MutableStateFlow(value = null) + + private val requireImplementation: UserWalletsListManager + get() = requireNotNull(implementation.value) { + "UserWalletsListManager is not initialized" + } init { subscribeOnCurrentManager() } + override val isLockable: Boolean + get() = requireImplementation.isLockable + override val userWallets: Flow> - get() = implementation.flatMapLatest { it.userWallets } + get() = implementation.transformLatest { impl -> + if (impl != null && impl.hasUserWallets) { + emitAll(impl.userWallets) + } + } + + override val userWalletsSync: List + get() = requireImplementation.userWalletsSync override val selectedUserWallet: Flow - get() = implementation.flatMapLatest { it.selectedUserWallet } + get() = implementation.transformLatest { impl -> + if (impl != null && impl.hasUserWallets) { + emitAll(impl.selectedUserWallet) + } + } override val selectedUserWalletSync: UserWallet? - get() = implementation.value.selectedUserWalletSync + get() = requireImplementation.selectedUserWalletSync override val hasUserWallets: Boolean - get() = implementation.value.hasUserWallets + get() = requireImplementation.hasUserWallets override val walletsCount: Int - get() = implementation.value.walletsCount + get() = requireImplementation.walletsCount override val isLocked: Flow - get() = implementation.flatMapLatest { - if (it is UserWalletsListManager.Lockable) { - it.isLocked + get() = implementation.transformLatest { impl -> + if (impl == null) return@transformLatest + + if (impl is UserWalletsListManager.Lockable) { + emitAll(impl.isLocked) } else { error("RuntimeUserWalletsListManager is not lockable") } @@ -65,43 +86,45 @@ internal class GeneralUserWalletsListManager( override val isLockedSync: Boolean get() { - val implementation = implementation.value - return if (implementation is UserWalletsListManager.Lockable) { - implementation.isLockedSync + val impl = requireImplementation + + return if (impl is UserWalletsListManager.Lockable) { + impl.isLockedSync } else { error("RuntimeUserWalletsListManager is not lockable") } } override suspend fun select(userWalletId: UserWalletId): CompletionResult { - return implementation.value.select(userWalletId) + return requireImplementation.select(userWalletId) } override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { - return implementation.value.save(userWallet, canOverride) + return requireImplementation.save(userWallet, canOverride) } override suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, ): CompletionResult { - return implementation.value.update(userWalletId, update) + return requireImplementation.update(userWalletId, update) } override suspend fun delete(userWalletIds: List): CompletionResult { - return implementation.value.delete(userWalletIds) + return requireImplementation.delete(userWalletIds) } override suspend fun clear(): CompletionResult { - return implementation.value.clear() + return requireImplementation.clear() } override suspend fun get(userWalletId: UserWalletId): CompletionResult { - return implementation.value.get(userWalletId) + return requireImplementation.get(userWalletId) } override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult { - val implementation = implementation.value + val implementation = requireImplementation + return if (implementation is UserWalletsListManager.Lockable) { implementation.unlock(type) } else { @@ -110,7 +133,8 @@ internal class GeneralUserWalletsListManager( } override fun lock() { - val implementation = implementation.value + val implementation = requireImplementation + return if (implementation is UserWalletsListManager.Lockable) { implementation.lock() } else { @@ -118,10 +142,6 @@ internal class GeneralUserWalletsListManager( } } - override fun isLockable(): Boolean { - return implementation.value.isLockable() - } - private fun subscribeOnCurrentManager() { appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) .distinctUntilChanged() @@ -144,17 +164,17 @@ internal class GeneralUserWalletsListManager( destinationManager = possibleManager, ) - previousManager.clear() + previousManager?.clear() } .flowOn(dispatchers.io) .launchIn(applicationScope) } private suspend fun copySelectedUserWallet( - sourceManager: UserWalletsListManager, + sourceManager: UserWalletsListManager?, destinationManager: UserWalletsListManager, ): UserWalletsListManager { - sourceManager.selectedUserWalletSync?.let { selectedWallet -> + sourceManager?.selectedUserWalletSync?.let { selectedWallet -> destinationManager.save(selectedWallet, canOverride = true) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index 31ef3366dd..4d211d9b88 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.* internal class RuntimeUserWalletsListManager : UserWalletsListManager { private val state = MutableStateFlow(State()) + override val isLockable: Boolean = false + override val userWallets: Flow> get() = state .mapLatest { listOfNotNull(it.userWallet) } @@ -24,6 +26,9 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { .filterNotNull() .distinctUntilChanged() + override val userWalletsSync: List + get() = listOfNotNull(state.value.userWallet) + override val selectedUserWalletSync: UserWallet? get() = state.value.userWallet @@ -34,7 +39,7 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { * only 1 wallet stored in runtime implementation */ override val walletsCount: Int - get() = 1 + get() = if (hasUserWallets) 1 else 0 override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { state.value.userWallet @@ -85,10 +90,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { state.value.userWallet ?: walletNotFound() } - override fun isLockable(): Boolean { - return false - } - private fun saveInternal(userWallet: UserWallet): CompletionResult = catching { state.update { prevState -> prevState.copy( diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 451e717e9e..7b869eb69f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -61,4 +61,14 @@ internal fun List.updateWith( ?: wallet } } -} \ No newline at end of file +} + +internal fun List.lockAll(): List = map(UserWallet::lock) + +internal fun UserWallet.lock(): UserWallet = copy( + scanResponse = scanResponse.copy( + card = scanResponse.card.copy( + wallets = emptyList(), + ), + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt index 16a35bf539..2b3094d9f6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt @@ -10,13 +10,11 @@ import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTrans import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData import com.tangem.tap.features.details.redux.walletconnect.TradeData -import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder import timber.log.Timber internal object BnbHelper { - fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer { + fun createMessageData(order: WcBinanceTransferOrder): BinanceMessageData.Transfer { val input = order.msgs.first().inputs.first() val output = order.msgs.first().inputs.first() @@ -42,51 +40,7 @@ internal object BnbHelper { ) } - fun WcBinanceTradeOrder.toWCBinanceTradeOrder(): WCBinanceTradeOrder { - return WCBinanceTradeOrder( - account_number = accountNumber, - chain_id = chainId, - data = data, - memo = memo, - sequence = sequence, - source = source, - msgs = msgs.map { it.toWCBinanceTradeOrderMessage() }, - ) - } - - private fun WcBinanceTradeOrder.Message.toWCBinanceTradeOrderMessage(): WCBinanceTradeOrder.Message { - return WCBinanceTradeOrder.Message(id, orderType, price, quantity, sender, side, symbol, timeInforce) - } - - fun WcBinanceTransferOrder.toWCBinanceTransferOrder(): WCBinanceTransferOrder { - return WCBinanceTransferOrder( - account_number = accountNumber, - chain_id = chainId, - data = data, - memo = memo, - sequence = sequence, - source = source, - msgs = msgs.map { it.toWCBinanceTransferOrderMessage() }, - ) - } - - private fun WcBinanceTransferOrder.Message.toWCBinanceTransferOrderMessage(): WCBinanceTransferOrder.Message { - return WCBinanceTransferOrder.Message( - inputs.map { it.toWCBinanceItem() }, - outputs.map { it.toWCBinanceItem() }, - ) - } - - private fun WcBinanceTransferOrder.Message.Item.toWCBinanceItem(): WCBinanceTransferOrder.Message.Item { - return WCBinanceTransferOrder.Message.Item( - address, - coins.map { - WCBinanceTransferOrder.Message.Item.Coin(it.amount, it.denom) - }, - ) - } - - fun createMessageData(order: WCBinanceTradeOrder): BinanceMessageData.Trade { + fun createMessageData(order: WcBinanceTradeOrder): BinanceMessageData.Trade { val address = order.msgs.first().sender val tradeData = order.msgs.map { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt deleted file mode 100644 index 9078975c2c..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ /dev/null @@ -1,562 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.guard -import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.walletconnect.extensions.isDappSupported -import com.tangem.tap.domain.walletconnect.extensions.toWcEthereumSignMessage -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData -import com.tangem.tap.features.details.redux.walletconnect.* -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.walletConnectRepository -import com.trustwallet.walletconnect.WCClient -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage -import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction -import com.trustwallet.walletconnect.models.session.WCAddNetwork -import com.trustwallet.walletconnect.models.session.WCSession -import com.trustwallet.walletconnect.models.session.WCSessionUpdate -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit -import kotlin.collections.set - -@Suppress("LargeClass") -internal class WalletConnectManager { - - private var cardId: String? = null - - private val okHttpClient: OkHttpClient by lazy { - OkHttpClient.Builder() - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(20, TimeUnit.SECONDS) - .writeTimeout(20, TimeUnit.SECONDS) - .addInterceptor(interceptor) - .addInterceptor(RetryInterceptor()) - .build() - } - - private val interceptor by lazy { - createNetworkLoggingInterceptor() - } - - private var sessions: MutableMap = mutableMapOf() - - fun connect(wcUri: String) { - val session = WCSession.from(wcUri).guard { - store.dispatchOnMain( - WalletConnectAction.FailureEstablishingSession( - session = null, - error = TapError.WalletConnect.UnsupportedLink, - ), - ) - return - } - if (sessions[session.topic] != null) { - store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession) - return - } - val client = WCClient(httpClient = okHttpClient) - setListeners(client) - val peerId = UUID.randomUUID().toString() - - try { - client.connect(session, tangemPeerMeta, peerId) - } catch (exception: IllegalArgumentException) { - store.dispatchOnMain( - WalletConnectAction.FailureEstablishingSession( - session = null, - error = TapError.WalletConnect.UnsupportedLink, - ), - ) - return - } - - sessions[session.topic] = WalletConnectActiveData( - peerId = peerId, - remotePeerId = null, - session = session, - client = client, - wallet = WalletForSession(), - ) - setupConnectionTimeoutCheck(session) - } - - fun updateSession(session: WalletConnectSession) { - val updatedSession = sessions[session.session.topic]?.copy( - wallet = session.wallet, - ) - if (updatedSession != null) { - sessions[session.session.topic] = updatedSession - } - } - - fun updateBlockchain(session: WalletConnectSession) { - sessions[session.session.topic]?.client?.updateSession( - accounts = listOfNotNull(session.getAddress()), - chainId = session.wallet.blockchain?.getChainId(), - approved = true, - ) - - val updatedSession = sessions[session.session.topic]?.copy( - wallet = session.wallet, - ) - if (updatedSession != null) { - sessions[session.session.topic] = updatedSession - } - } - - @Suppress("MagicNumber") - private fun setupConnectionTimeoutCheck(session: WCSession) { - scope.launch { - delay(20_000) - val data = sessions[session.topic] - if (data != null && data.peerMeta == null) { - disconnect(session) - store.dispatchOnMain(WalletConnectAction.OpeningSessionTimeout(session)) - } - } - } - - fun restoreSessions(scanResponse: ScanResponse) { - val walletPublicKey = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }?.publicKey - ?: return - if (scanResponse.card.backupStatus?.isActive != true) cardId = scanResponse.card.cardId - val sessions = walletConnectRepository.loadSavedSessions() - // filter sessions for this particular card - .filter { it.wallet.walletPublicKey.contentEquals(walletPublicKey) } - this.sessions = sessions - .map { session -> - WalletConnectActiveData( - peerId = session.peerId, - remotePeerId = session.remotePeerId, - client = WCClient(httpClient = okHttpClient), - session = session.session, - peerMeta = session.peerMeta, - wallet = session.wallet, - ) - .also { - setListeners(it.client) - it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId) - } - }.associateBy { it.session.topic }.toMutableMap() - - store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions)) - } - - fun approve(session: WCSession) { - val activeData = sessions[session.topic] ?: return - removeSimilarSessions(activeData) - - val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey ?: return - val blockchain = activeData.wallet.getBlockchainForSession() - val accounts = listOf(blockchain.makeAddresses(key).first().value) - val approved = activeData.client.approveSession( - accounts = accounts, - chainId = blockchain.getChainId() ?: Blockchain.Ethereum.getChainId()!!, - ) - if (approved) { - val walletConnectSession = WalletConnectSession( - peerId = activeData.peerId, - remotePeerId = activeData.remotePeerId, - wallet = activeData.wallet, - session = session, - peerMeta = activeData.peerMeta!!, - ) - walletConnectRepository.saveSession(walletConnectSession) - store.dispatchOnMain( - WalletConnectAction.ApproveSession.Success( - walletConnectSession, - ), - ) - } - } - - private fun removeSimilarSessions(activeData: WalletConnectActiveData) { - val sessionsToRemove = sessions.filter { - it.value.wallet.walletPublicKey?.equals(activeData.wallet.walletPublicKey) == true && - it.value.peerMeta?.url == activeData.peerMeta?.url && - it.value.session != activeData.session - } - Timber.d("RemoveSimilarSessions: ${sessionsToRemove.values.map { it.client.session }}") - sessionsToRemove.forEach { disconnect(it.value.session) } - } - - fun rejectRequest(topic: String, id: Long) { - val activeData = sessions[topic] ?: return - activeData.client.rejectRequest(id) - } - - private fun acceptRequest(topic: String, id: Long, data: String) { - val activeData = sessions[topic] ?: return - activeData.client.approveRequest(id, data) - } - - fun disconnect(session: WCSession) { - val activeData = sessions[session.topic] ?: return - val disconnected = if (activeData.client.isConnected) { - activeData.client.killSession() - } else { - true - } - - if (disconnected) { - onSessionClosed(session) - } - } - - private fun onSessionClosed(session: WCSession) { - sessions.remove(session.topic) - walletConnectRepository.removeSession(session) - store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) - } - - fun handleTransactionRequest( - transaction: WcEthereumTransaction, - session: WalletConnectSession, - id: Long, - type: WcEthTransactionType, - ) { - val activeData = sessions[session.session.topic] ?: return - scope.launch { - val data = WalletConnectSdkHelper().prepareTransactionData( - EthTransactionData( - transaction = transaction, - networkId = session.wallet.blockchain?.toNetworkId() ?: "", - rawDerivationPath = session.wallet.derivationPath?.rawPath, - id = id, - topic = session.session.topic, - type = type, - metaName = session.peerMeta.name, - metaUrl = session.peerMeta.url, - ), - ).guard { - sessions[session.session.topic] = activeData.copy(transactionData = null) - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - session.session.topic, - id, - ), - ) - return@launch - } - sessions[session.session.topic] = activeData.copy(transactionData = data) - - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.RequestTransaction( - WcPreparedRequest.EthTransaction( - preparedRequestData = data, - topic = session.session.topic, - requestId = id, - derivationPath = data.walletManager.wallet.publicKey.derivationPath?.rawPath, - ), - ), - ), - ) - } - } - - fun completeTransaction(topic: Topic) { - val activeData = sessions[topic] - val data = activeData?.transactionData ?: return - scope.launch { - val hash = WalletConnectSdkHelper().completeTransaction(data, cardId).guard { - sessions[topic] = activeData.copy(transactionData = null) - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - topic, - data.id, - ), - ) - return@launch - } - acceptRequest(topic, data.id, hash) - sessions[topic] = activeData.copy(transactionData = null) - } - } - - fun signBnb(id: Long, data: ByteArray, topic: Topic) { - val activeData = sessions[topic] ?: return - scope.launch { - val hash = WalletConnectSdkHelper().signBnbTransaction( - data = data, - networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", - derivationPath = activeData.wallet.derivationPath?.rawPath, - cardId = cardId, - ).guard { - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - topic, - id, - ), - ) - return@launch - } - acceptRequest(topic, id, hash) - } - } - - fun handlePersonalSignRequest(message: WCEthereumSignMessage, session: WalletConnectSession, id: Long) { - val activeData = sessions[session.session.topic] ?: return - scope.launch { - val data = WalletConnectSdkHelper().prepareDataForPersonalSign( - message = message.toWcEthereumSignMessage(), - topic = session.session.topic, - id = id, - metaName = session.peerMeta.name, - ) - sessions[session.session.topic] = activeData.copy(personalSignData = data) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.PersonalSign( - WcPreparedRequest.EthSign( - preparedRequestData = data, - topic = session.session.topic, - requestId = id, - derivationPath = session.wallet.derivationPath?.rawPath, - ), - ), - ), - ) - } - } - - fun sendSignedMessage(topic: Topic) { - val activeData = sessions[topic] - val data = activeData?.personalSignData ?: return - scope.launch { - val hash = WalletConnectSdkHelper().signPersonalMessage( - hashToSign = data.hash, - networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", - type = data.type, - derivationPath = activeData.wallet.derivationPath?.rawPath, - cardId = cardId, - ) - .guard { - sessions[topic] = activeData.copy(transactionData = null) - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - topic, - data.id, - ), - ) - return@launch - } - sessions[topic] = activeData.copy(personalSignData = data) - acceptRequest(topic, data.id, hash) - sessions[topic] = activeData.copy(transactionData = null) - } - } - - @Suppress("LongMethod", "ComplexMethod") - private fun setListeners(client: WCClient) { - client.onSessionRequest = { id: Long, peer: WCPeerMeta -> - Timber.d("OnSessionRequest: $peer") - val session = client.session - val data = sessions[session?.topic]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId) - if (data != null && session != null) { - if (!peer.isDappSupported()) { - store.dispatchOnMain( - WalletConnectAction.FailureEstablishingSession( - session = session, - error = TapError.WalletConnect.UnsupportedDapp, - ), - ) - } else { - sessions[session.topic] = data - val sessionData = data.toWalletConnectSession() - sessionData?.let { - store.dispatchOnMain( - WalletConnectAction.ScanCard( - session = sessionData, - chainId = client.chainId?.toIntOrNull(), - ), - ) - } - } - } - } - client.onSessionUpdate = { id: Long, update: WCSessionUpdate -> - Timber.d("onSessionUpdate: $update") - val session = client.session - if (session != null && !update.approved) onSessionClosed(session) - } - client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction -> - Timber.d("onEthSendTransaction: $transaction") - // Analytics.logWcEvent( - // AnalyticsAnOld.WcAnalyticsEvent.Action( - // AnalyticsAnOld.WcAction.SendTransaction - // ) - // ) - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandleTransactionRequest( - transaction = transaction, - session = sessionData, - id = id, - type = WcEthTransactionType.EthSendTransaction, - ), - ) - } - } - client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction -> - Timber.d("onEthSignTransaction: $transaction") - // Analytics.logWcEvent( - // AnalyticsAnOld.WcAnalyticsEvent.Action( - // AnalyticsAnOld.WcAction.SignTransaction - // ) - // ) - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandleTransactionRequest( - transaction = transaction, - session = sessionData, - id = id, - type = WcEthTransactionType.EthSignTransaction, - ), - ) - } - } - client.onEthSign = { id: Long, message: WCEthereumSignMessage -> - Timber.d("onEthSign: $message") - // Analytics.logWcEvent( - // AnalyticsAnOld.WcAnalyticsEvent.Action( - // AnalyticsAnOld.WcAction.PersonalSign - // ) - // ) - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandlePersonalSignRequest( - message, - sessionData, - id, - ), - ) - } - } - - client.onBnbCancel = { id: Long, order: WCBinanceCancelOrder -> - } - client.onBnbTrade = { id: Long, order: WCBinanceTradeOrder -> - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.BinanceTransaction.Trade( - id = id, - order = order, - sessionData = sessionData, - ), - ) - } - } - client.onBnbTransfer = { id: Long, order: WCBinanceTransferOrder -> - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.BinanceTransaction.Transfer( - id = id, - order = order, - sessionData = sessionData, - ), - ) - } - } - client.onBnbTxConfirm = { id: Long, order: WCBinanceTxConfirmParam -> - // send empty approve request if status is OK - if (order.ok) client.approveRequest(id, "") - } - client.onDisconnect = { code: Int, reason: String -> - val session = client.session - if (session != null) { - onSessionClosed(session) - } - } - client.onWalletChangeNetwork = { id: Long, chainId: Int -> - switchChain(chainId, client) - } - client.onWalletAddNetwork = { id: Long, network: WCAddNetwork -> - // TODO - // In fact this method is used to add a EVM network. It provides RPC url and chain ID. - // Here now it just tries to switch to a EVM network with a provided chain ID. - try { - val chainId = Integer.decode(network.chainIdHex) - switchChain(chainId, client) - } catch (exception: Exception) { - Timber.d("WC add network error: chain could not be parsed") - } - } - } - - private fun switchChain(chainId: Int, client: WCClient) { - val blockchain = Blockchain.fromChainId(chainId) - Timber.d("WC switch chainID\nNew Blockchain: $blockchain") - val session = sessions[client.session?.topic]?.toWalletConnectSession() - if (session != null) { - store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session)) - } - } - - companion object { - const val WC_SCHEME = "wc" - - private val tangemPeerMeta = WCPeerMeta(name = "Tangem Wallet", url = "https://tangem.com") - - fun isCorrectWcUri(string: String): Boolean = WCSession.from(string) != null - } -} - -internal typealias Topic = String - -internal data class WalletConnectActiveData( - val peerId: String, - val remotePeerId: String?, - val client: WCClient, - val session: WCSession, - val peerMeta: WCPeerMeta? = null, - val wallet: WalletForSession, - val transactionData: WcTransactionData? = null, - val personalSignData: WcPersonalSignData? = null, -) { - fun toWalletConnectSession(): WalletConnectSession? { - if (peerMeta == null) return null - return WalletConnectSession( - peerId = peerId, - remotePeerId = remotePeerId, - wallet = wallet, - session = session, - peerMeta = peerMeta, - ) - } -} - -@Suppress("MagicNumber") -internal class RetryInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request: Request = chain.request() - val response = chain.proceed(request) - when (response.code) { - 502 -> { - return chain.proceed(request) - } - } - return response - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt deleted file mode 100644 index 19e2e44e42..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.tangem.blockchain.common.Blockchain -import com.trustwallet.walletconnect.models.WCPeerMeta - -object WalletConnectNetworkUtils { - fun parseBlockchain(chainId: Int?, peer: WCPeerMeta): Blockchain? { - return when { - peer.url.contains("pancakeswap.finance") -> { - Blockchain.BSC - } - peer.url.contains("optimism") -> { - Blockchain.Optimism - } - chainId != null -> { - Blockchain.fromChainId(chainId) - } - peer.url.contains("matic.network") || peer.name == "Polygon" -> { - Blockchain.Polygon - } - peer.url.contains("binance.org") || peer.name.contains("Binance") -> { - if (peer.icons.firstOrNull()?.contains("testnet") == true) { - Blockchain.BinanceTestnet - } else { - Blockchain.Binance - } - } - peer.name.contains("BSC") -> { - Blockchain.BSC - } - peer.url.contains("honeyswap.1hive.eth.limo") -> { - // Check if something's changed after this bug report: - // https://github.com/1Hive/honeyswap-interface/issues/83 - Blockchain.Gnosis - } - else -> { - Blockchain.Ethereum - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt deleted file mode 100644 index de77928810..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import android.app.Application -import android.content.Context -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Types -import com.tangem.common.extensions.hexToBytes -import com.tangem.common.extensions.toHexString -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.features.details.redux.walletconnect.WalletForSession -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.session.WCSession -import timber.log.Timber -import java.io.FileNotFoundException -import java.nio.charset.Charset - -class WalletConnectRepository(val context: Application) { - private val walletConnectAdapter: JsonAdapter> = MoshiConverter.sdkMoshi.adapter( - Types.newParameterizedType(List::class.java, SessionDao::class.java), - ) - - fun saveSession(session: WalletConnectSession) { - val sessions = loadSavedSessions() + session - saveSessions(sessions) - } - - fun removeSession(session: WCSession) { - val sessions = loadSavedSessions().filterNot { it.session == session } - saveSessions(sessions) - } - - fun loadSavedSessions(): List { - return try { - val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS) - .hexToUtf8() - walletConnectAdapter.fromJson(json)!!.map { it.toSession() } - } catch (e: FileNotFoundException) { - emptyList() - } catch (exception: Exception) { - Timber.w(exception) - emptyList() - } - } - - private fun saveSessions(sessions: List) { - val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) }) - .utf8ToHex() // convert to hex to solve problems with saving text with emojis - Timber.e("WC sessions, saving following json: $json") - context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS) - } - - private fun String.utf8ToHex(): String { - return this.toByteArray().toHexString() - } - - private fun String.hexToUtf8(): String { - return this.hexToBytes().toString(Charset.defaultCharset()) - } - - private fun Context.readFileText(fileName: String): String = - this.openFileInput(fileName).bufferedReader().readText() - - private fun Context.rewriteFile(content: String, fileName: String) { - this.openFileOutput(fileName, Context.MODE_PRIVATE).use { - it.write(content.toByteArray(), 0, content.length) - } - } - - companion object { - private const val FILE_NAME_PREFIX_SESSIONS = "wc_sessions" - } -} - -@JsonClass(generateAdapter = true) -data class SessionDao( - val peerId: String, - val remotePeerId: String?, - val wallet: WalletForSession, - val session: WCSession, - val peerMeta: WCPeerMeta, -) { - fun toSession(): WalletConnectSession { - return WalletConnectSession( - peerId = peerId, - remotePeerId = remotePeerId, - wallet = wallet, - session = session, - peerMeta = peerMeta, - ) - } - - companion object { - fun fromSession(session: WalletConnectSession): SessionDao { - return SessionDao( - peerId = session.peerId, - remotePeerId = session.remotePeerId, - wallet = session.wallet, - session = session.session, - peerMeta = session.peerMeta, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 12dc242d80..abb7d6ce7e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -8,6 +8,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.* +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.CompletionResult import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey @@ -15,13 +16,10 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString -import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder -import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder import com.tangem.tap.domain.walletconnect2.domain.TransactionType import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage @@ -38,7 +36,6 @@ import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionReque import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userWalletsListManager import timber.log.Timber import java.math.BigDecimal import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam @@ -46,20 +43,35 @@ import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam @Suppress("LargeClass") class WalletConnectSdkHelper { + private val userWalletsListManager by lazy { + store.inject(DaggerGraphState::generalUserWalletsListManager) + } + @Suppress("MagicNumber") - suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData? { + suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData { val transaction = data.transaction - val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null - val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null + val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { + "Blockchain not found" + } + val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) { + "WalletManager not found" + } walletManager.safeUpdate(isDemoCard()) val wallet = walletManager.wallet - val balance = wallet.amounts[AmountType.Coin]?.value ?: return null + val balance = requireNotNull(wallet.amounts[AmountType.Coin]?.value) { + "Coin balance not found" + } val decimals = wallet.blockchain.decimals() - val value = (transaction.value ?: "0").hexToBigDecimal() - .movePointLeft(decimals) ?: return null + val value = (transaction.value ?: "0") + .hexToBigDecimal() + .movePointLeft(decimals) + + requireNotNull(value) { + "Transaction amount is null" + } val gasLimit = getGasLimitFromTx(value, walletManager, transaction) @@ -68,20 +80,23 @@ class WalletConnectSdkHelper { is Result.Success -> result.data.toBigDecimal() is Result.Failure -> { (result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") } - return null + + error("Unable to get gas price: ${result.error}") } - null -> return null + null -> error("Gas price is null") } val fee = (gasLimit * gasPrice).movePointLeft(decimals) val total = value + fee + val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" } + val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), // TODO refactoring fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, - destinationAddress = transaction.to!!, + destinationAddress = destinationAddress, extras = EthereumTransactionExtras( data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(), gasLimit = gasLimit.toBigInteger(), @@ -101,6 +116,7 @@ class WalletConnectSdkHelper { id = data.id, type = data.type, ) + return WcTransactionData( type = data.type, transaction = transactionData, @@ -176,17 +192,17 @@ class WalletConnectSdkHelper { ), ) return when (result) { - SimpleResult.Success -> { + is Result.Success -> { val sentFrom = CoreAnalyticsParam.TxSentFrom.WalletConnect Analytics.send(Basic.TransactionSent(sentFrom = sentFrom, memoType = MemoType.Null)) - val hash = data.walletManager.wallet.recentTransactions.last().hash - if (hash?.startsWith(HEX_PREFIX) == true) { + val hash = result.data.hash + if (hash.startsWith(HEX_PREFIX)) { hash } else { HEX_PREFIX + hash } } - is SimpleResult.Failure -> { + is Result.Failure -> { Timber.e(result.error as BlockchainSdkError) null } @@ -227,11 +243,11 @@ class WalletConnectSdkHelper { } fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade { - return BnbHelper.createMessageData(data.toWCBinanceTradeOrder()) + return BnbHelper.createMessageData(data) } fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer { - return BnbHelper.createMessageData(data.toWCBinanceTransferOrder()) + return BnbHelper.createMessageData(data) } suspend fun signBnbTransaction( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt deleted file mode 100644 index d227a42923..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.domain.walletconnect.extensions - -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction -import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage -import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction - -internal fun WCPeerMeta.isDappSupported(): Boolean { - return !unsupportedDappsList.any { this.url.contains(it) } -} - -private val unsupportedDappsList: List = listOf("dydx.exchange") - -internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction { - return WcEthereumTransaction( - from = from, - to = to, - nonce = nonce, - gasPrice = gasPrice, - maxFeePerGas = maxFeePerGas, - maxPriorityFeePerGas = maxPriorityFeePerGas, - gas = gas, - gasLimit = gasLimit, - value = value, - data = data, - ) -} - -internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage { - return WcSignMessage( - raw = raw, - type = when (type) { - WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE - WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE - WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index 63e68ba973..edddd1a886 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -1,13 +1,13 @@ package com.tangem.tap.domain.walletconnect2.app import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles internal class TangemWcBlockchainHelper( - private val featureToggles: WalletConnectFeatureToggles, + featureToggles: WalletConnectFeatureToggles, ) : WcBlockchainHelper { private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 0fb1675a82..922581137c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -4,7 +4,7 @@ import android.app.Application import arrow.core.flatten import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.events.WalletConnect -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.domain.WcRequest @@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import timber.log.Timber -internal class DefaultWalletConnectRepository( +internal class DefaultLegacyWalletConnectRepository( private val application: Application, private val wcRequestDeserializer: WcJrpcRequestsDeserializer, private val analyticsHandler: AnalyticsEventHandler, -) : WalletConnectRepository { +) : LegacyWalletConnectRepository { private var sessionProposal: Wallet.Model.SessionProposal? = null private var userNamespaces: Map>? = null @@ -94,7 +94,7 @@ internal class DefaultWalletConnectRepository( ) { // Triggered when wallet receives the session proposal sent by a Dapp Timber.d("sessionProposal: $sessionProposal") - this@DefaultWalletConnectRepository.sessionProposal = sessionProposal + this@DefaultLegacyWalletConnectRepository.sessionProposal = sessionProposal if (sessionProposal.name in unsupportedDApps) { Timber.w("Unsupported DApp") @@ -110,7 +110,7 @@ internal class DefaultWalletConnectRepository( val missingNetworks = findMissingNetworks( namespaces = sessionProposal.requiredNamespaces, - userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(), + userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(), ) if (missingNetworks.isNotEmpty()) { @@ -127,7 +127,7 @@ internal class DefaultWalletConnectRepository( val optionalWithoutMissingNetworks = removeMissingNetworks( namespaces = sessionProposal.optionalNamespaces, - userNamespaces = this@DefaultWalletConnectRepository.userNamespaces ?: emptyMap(), + userNamespaces = this@DefaultLegacyWalletConnectRepository.userNamespaces ?: emptyMap(), ) scope.launch { @@ -375,6 +375,7 @@ internal class DefaultWalletConnectRepository( override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { val session = currentSessions.find { it.topic == requestData.topic } + analyticsHandler.send( WalletConnect.RequestHandled( WalletConnect.RequestHandledParams( @@ -386,17 +387,18 @@ internal class DefaultWalletConnectRepository( ), ), ) - cancelRequest(requestData.topic, requestData.requestId) + + cancelRequest(requestData.topic, requestData.requestId, error.error) } - override fun cancelRequest(topic: String, id: Long) { + override fun cancelRequest(topic: String, id: Long, message: String) { Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( sessionTopic = topic, jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( id = id, code = 0, - message = "", + message = message, ), ), onSuccess = {}, @@ -407,8 +409,8 @@ internal class DefaultWalletConnectRepository( override fun reject() { Web3Wallet.rejectSession( params = Wallet.Params.SessionReject( - sessionProposal?.proposerPublicKey ?: "", - "", + proposerPublicKey = sessionProposal?.proposerPublicKey ?: "", + reason = "", ), onSuccess = { Timber.d("Rejected successfully: $it") 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 927eab61da..d4fef91bab 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 @@ -6,13 +6,16 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.files.FileReader +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl -import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles @@ -32,9 +35,12 @@ internal object WalletConnectInteractorModule { @Provides @ActivityScoped fun provideWalletConnectInteractor( - wcRepository: WalletConnectRepository, + wcRepository: LegacyWalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, walletConnectFeatureToggles: WalletConnectFeatureToggles, + currenciesRepository: CurrenciesRepository, + walletManagersFacade: WalletManagersFacade, + userWalletsListManager: UserWalletsListManager, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -42,7 +48,10 @@ internal object WalletConnectInteractorModule { sessionsRepository = wcSessionsRepository, sdkHelper = WalletConnectSdkHelper(), blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles), - dispatcher = AppCoroutineDispatcherProvider(), + currenciesRepository = currenciesRepository, + walletManagersFacade = walletManagersFacade, + userWalletsListManager = userWalletsListManager, + dispatchers = AppCoroutineDispatcherProvider(), ) } } @@ -63,8 +72,8 @@ internal object WalletConnectModule { application: Application, wcRequestDeserializer: WcJrpcRequestsDeserializer, analyticsHandler: AnalyticsEventHandler, - ): WalletConnectRepository { - return DefaultWalletConnectRepository( + ): LegacyWalletConnectRepository { + return DefaultLegacyWalletConnectRepository( application = application, wcRequestDeserializer = wcRequestDeserializer, analyticsHandler = analyticsHandler, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt similarity index 86% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 33d62fca22..9096e879e0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.tap.domain.walletconnect2.domain.models.* import kotlinx.coroutines.flow.Flow -interface WalletConnectRepository { +interface LegacyWalletConnectRepository { val events: Flow @@ -27,5 +27,5 @@ interface WalletConnectRepository { fun rejectRequest(requestData: RequestData, error: WalletConnectError) - fun cancelRequest(topic: String, id: Long) + fun cancelRequest(topic: String, id: Long, message: String = "") } \ No newline at end of file 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 3bf99ffd83..b63a1d0005 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 @@ -1,26 +1,52 @@ package com.tangem.tap.domain.walletconnect2.domain +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onEach +import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +@Suppress("LargeClass", "LongParameterList") class WalletConnectInteractor( private val handler: WalletConnectEventsHandler, - private val walletConnectRepository: WalletConnectRepository, + private val walletConnectRepository: LegacyWalletConnectRepository, private val sessionsRepository: WalletConnectSessionsRepository, private val sdkHelper: WalletConnectSdkHelper, - private val dispatcher: CoroutineDispatcherProvider, + private val dispatchers: CoroutineDispatcherProvider, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val userWalletsListManager: UserWalletsListManager, val blockchainHelper: WcBlockchainHelper, ) { + private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { + GetSelectedWalletUseCase(userWalletsListManager) + } + + private val wcScope = CoroutineScope( + Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"), + ) + + private val listenerScope = CoroutineScope( + Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"), + ) + private val events = walletConnectRepository.events private val sessions = walletConnectRepository.activeSessions @@ -34,19 +60,56 @@ class WalletConnectInteractor( sdkHelper = sdkHelper, ) - suspend fun startListening(userWalletId: String, cardId: String?) { + init { + getSelectedWalletUseCase().onRight { userWalletFlow -> + userWalletFlow + .conflate() + .distinctUntilChanged() + .onEach(::initWithWallet) + .flowOn(dispatchers.io) + .launchIn(wcScope) + } + } + + private suspend fun initWithWallet(userWallet: UserWallet) { + if (userWallet.isMultiCurrency) { + Timber.d("WalletConnect: initialize and setup networks for ${userWallet.walletId}") + startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet)) + subscribeOnCurrenciesUpdates(userWallet) + } + } + + private fun subscribeOnCurrenciesUpdates(userWallet: UserWallet) { + currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach { currencies -> + setupUserChains(userWallet, currencies) + } + .flowOn(dispatchers.io) + .launchIn(wcScope) + } + + private suspend fun setupUserChains(userWallet: UserWallet, currencies: List) { + val accounts = getAccountsForWc( + userWallet = userWallet, + networks = currencies.map { it.network }, + ) + setUserChains(accounts) + } + + private suspend fun startListeningWc(userWalletId: String, cardId: String?) { this.userWalletId = userWalletId this.cardId = cardId - - coroutineScope { + listenerScope.coroutineContext.cancelChildren() + listenerScope.launch { launch { subscribeToEvents() } launch { subscribeToSessions() } - walletConnectRepository.updateSessions() } } - fun setUserChains(accounts: List) { + private fun setUserChains(accounts: List) { val userNamespaces: Map> = accounts .groupBy { account -> blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) @@ -106,7 +169,7 @@ class WalletConnectInteractor( } } } - .flowOn(dispatcher.io) + .flowOn(dispatchers.io) .collect() } @@ -117,7 +180,7 @@ class WalletConnectInteractor( val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics) handler.onListOfSessionsUpdated(filteredSessions) } - .flowOn(dispatcher.io) + .flowOn(dispatchers.io) .collect() } @@ -197,10 +260,18 @@ class WalletConnectInteractor( ) else -> { currentRequest = sessionRequest - val data = prepareRequestData(sessionRequest) - if (data != null) { - handler.onSessionRequest(data) + + val data = prepareRequestData(sessionRequest).getOrElse { e -> + val wrappedError = e as? WalletConnectError ?: WalletConnectError.UnknownError( + message = e.localizedMessage ?: "Unknown error", + ) + + walletConnectRepository.rejectRequest(requestData, wrappedError) + handler.onSessionRejected(wrappedError) + return } + + handler.onSessionRequest(data) } } } @@ -258,7 +329,41 @@ class WalletConnectInteractor( return uri.lowercase().startsWith(WC_SCHEME) } - private suspend fun prepareRequestData(sessionRequest: WalletConnectEvents.SessionRequest): WcPreparedRequest? { + private fun getCardId(userWallet: UserWallet): String? { + return if (userWallet.scanResponse.card.backupStatus?.isActive != true) { + userWallet.cardId + } else { // if wallet has backup, any card from wallet can be used to sign + null + } + } + + private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List): List { + val walletManagers = networks.mapNotNull { + val blockchain = Blockchain.fromId(it.id.value) + walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + blockchain = blockchain, + derivationPath = it.derivationPath.value, + ) + } + return walletManagers.mapNotNull { + val wallet = it.wallet + val chainId = blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } + } + } + + private suspend fun prepareRequestData( + sessionRequest: WalletConnectEvents.SessionRequest, + ): Result { return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt index 12085cad12..e02d7cc679 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -4,6 +4,7 @@ import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType @@ -17,15 +18,18 @@ internal class WcSessionRequestConverter( suspend fun prepareRequest( sessionRequest: WalletConnectEvents.SessionRequest, userWalletId: String, - ): WcPreparedRequest? { - val networkId = blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId ?: "") ?: return null + ): Result = runCatching { + val networkId = requireNotNull(blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId.orEmpty())) { + "Failed to get network ID for chain ID: ${sessionRequest.chainId}" + } val derivationPath = getDerivationPath( sessionsRepository = sessionsRepository, sessionRequest = sessionRequest, userWalletId = userWalletId, walletAddress = getWalletAddress(sessionRequest.request), ) - return when (val request = sessionRequest.request) { + + when (val request = sessionRequest.request) { is WcRequest.EthSendTransaction -> { val data = sdkHelper.prepareTransactionData( EthTransactionData( @@ -38,7 +42,8 @@ internal class WcSessionRequestConverter( metaName = sessionRequest.metaName, metaUrl = sessionRequest.metaUrl, ), - ) ?: return null + ) + WcPreparedRequest.EthTransaction( preparedRequestData = data, topic = sessionRequest.topic, @@ -58,7 +63,8 @@ internal class WcSessionRequestConverter( metaName = sessionRequest.metaName, metaUrl = sessionRequest.metaUrl, ), - ) ?: return null + ) + WcPreparedRequest.EthTransaction( preparedRequestData = data, topic = sessionRequest.topic, @@ -122,7 +128,7 @@ internal class WcSessionRequestConverter( derivationPath = derivationPath, ) } - else -> null + else -> throw WalletConnectError.UnsupportedMethod } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt index 7d6504d923..40d82cdb51 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt @@ -20,8 +20,12 @@ sealed class WalletConnectError(val error: String) : Exception() { override val message: String?, ) : WalletConnectError("ExternalApprovalError") - object WrongUserWallet : WalletConnectError("WrongUserWallet") - object UnsupportedMethod : WalletConnectError("UnsupportedMethod") - object SigningError : WalletConnectError("SigningError") - object ValidationError : WalletConnectError("ValidationError") + data class UnknownError( + override val message: String, + ) : WalletConnectError(message) + + data object WrongUserWallet : WalletConnectError("WrongUserWallet") + data object UnsupportedMethod : WalletConnectError("UnsupportedMethod") + data object SigningError : WalletConnectError("SigningError") + data object ValidationError : WalletConnectError("ValidationError") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt index 96f2eea824..e9b45a2623 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.customtoken.impl.data import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index 2de3e037af..de73abac25 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.customtoken.impl.domain import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index 3900824553..da295c57c5 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel import dagger.hilt.android.AndroidEntryPoint @@ -23,7 +23,7 @@ import javax.inject.Inject internal class AddCustomTokenFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt index 55afd696af..afff4bf8e5 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity 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.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm @@ -70,7 +71,7 @@ internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, mod @Preview @Composable private fun Preview_AddCustomTokenContent() { - TangemTheme { + TangemThemePreview { AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt index 3824462da3..d2e3729321 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt @@ -1,11 +1,12 @@ package com.tangem.tap.features.customtoken.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder /** @@ -24,21 +25,12 @@ internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifi } @Preview(showSystemUi = true) +@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AddCustomTokenScreen_Light( +private fun Preview_AddCustomTokenScreen( @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, ) { - TangemTheme(isDark = false) { - AddCustomTokenScreen(stateHolder) - } -} - -@Preview(showSystemUi = true) -@Composable -private fun Preview_AddCustomTokenScreen_Dark( - @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, -) { - TangemTheme(isDark = true) { + TangemThemePreview { AddCustomTokenScreen(stateHolder) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt index 48ffb09110..3bb17cddb9 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock @@ -242,7 +243,7 @@ private fun TestBlock( @Preview @Composable private fun Preview_AddCustomTokenTestContent() { - TangemTheme { + TangemThemePreview { AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt index a55ee2f29e..3cc1d4847a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton import com.tangem.wallet.R @@ -42,7 +43,7 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, m private fun Preview_AddCustomTokenFloatingButton( @PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton, ) { - TangemTheme { + TangemThemePreview { AddCustomTokenFloatingButton(model) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt index dd1293ba65..58da365d81 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.common.compose.TangemTextFieldsDefault import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField @@ -195,7 +196,7 @@ private fun SelectorField(model: AddCustomTokenSelectorField) { @Preview @Composable private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: AddCustomTokenForm) { - TangemTheme { + TangemThemePreview { AddCustomTokenForm(model) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt index 81d0f52912..030f130a1a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -54,7 +55,7 @@ internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () - @Preview @Composable internal fun Preview_AddCustomTokenToolbar() { - TangemTheme { + TangemThemePreview { AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {}) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt index a00fc2d4fb..a323a8a982 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.customtoken.impl.presentation.ui.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -15,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData @@ -71,17 +73,10 @@ private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AddCustomTokenWarnings_Light() { - TangemTheme { - AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) - } -} - -@Preview -@Composable -private fun Preview_AddCustomTokenWarnings_Dark() { - TangemTheme(isDark = true) { +private fun Preview_AddCustomTokenWarnings() { + TangemThemePreview { AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt index 5db91d6354..966af2bd6a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt @@ -23,12 +23,12 @@ object ContractAddressValidator { private fun validateAddress(blockchain: Blockchain, address: String): Boolean { return when (blockchain) { - Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> { - SuccessAddressValidator.validate(address) - } - else -> { - blockchain.validateAddress(address) - } + Blockchain.Unknown, + Blockchain.Binance, + Blockchain.BinanceTestnet, + -> SuccessAddressValidator.validate(address) + Blockchain.Cardano -> blockchain.validateContractAddress(address) + else -> blockchain.validateAddress(address) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 3dd2804b29..34944cf67a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -10,13 +10,20 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter +import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.extensions.* +import com.tangem.domain.common.extensions.canHandleBlockchain +import com.tangem.domain.common.extensions.canHandleToken +import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency @@ -72,6 +79,8 @@ internal class AddCustomTokenViewModel @Inject constructor( private val actionsHandler = ActionsHandler(featureRouter) private val testActionsHandler = TestActionsHandler() private val formStateBuilder = FormStateBuilder() + private val hederaAddressConverter = HederaTokenAddressConverter() + private val cardanoTokenAddressConverter = CardanoTokenAddressConverter() private var currentCryptoCurrencies: List = emptyList() @@ -232,7 +241,7 @@ internal class AddCustomTokenViewModel @Inject constructor( ifRight = { it.scanResponse }, ) val derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle() - return listOf(defaultNetwork) + Blockchain.values() + return listOf(defaultNetwork) + Blockchain.entries .filter { blockchain -> scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) ?.contains(blockchain) == true && isDerivationPathNotEmpty(derivationStyle, blockchain) @@ -316,7 +325,7 @@ internal class AddCustomTokenViewModel @Inject constructor( type = DerivationPathSelectorType.CUSTOM, derivationPath = "", ), - ) + Blockchain.values() + ) + Blockchain.entries .filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() } @@ -360,7 +369,8 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun updateForm(address: String, selectedNetwork: Blockchain) { viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { - featureInteractor.findToken(address = address, blockchain = selectedNetwork) + val tokenAddress = convertTokenAddress(selectedNetwork, address) + featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork) } .onSuccess { token -> foundToken = token @@ -587,13 +597,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isTokenAlreadyAdded(): Boolean { + val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain + val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id + val contractAddress = convertTokenAddress( + blockchain = networkSelectorValue, + address = uiState.form.contractAddressInputField.value, + ) return currentCryptoCurrencies .filterIsInstance() .any { token -> - val contractAddress = uiState.form.contractAddressInputField.value - val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain - val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id - val sameId = if (!token.isCustom) { // todo after move foundToken to CryptoCurrency model, use only id foundToken?.id == token.id.rawCurrencyId @@ -844,8 +856,10 @@ internal class AddCustomTokenViewModel @Inject constructor( val currency = when (getCustomTokenType()) { CustomTokenType.TOKEN -> { - val contractAddress = foundToken?.network?.contractAddress - ?: uiState.form.contractAddressInputField.value + val contractAddress = convertTokenAddress( + blockchain = blockchain, + address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value, + ) CustomCurrency.CustomToken( token = Token( name = uiState.form.tokenNameInputField.value, @@ -883,6 +897,19 @@ internal class AddCustomTokenViewModel @Inject constructor( } } + private fun convertTokenAddress(blockchain: Blockchain, address: String): String { + return when (blockchain) { + Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) + Blockchain.Cardano -> { + cardanoTokenAddressConverter.convertToFingerprint( + address = address, + symbol = uiState.form.tokenSymbolInputField.value, + ) + } + else -> address + } + } + private inner class TestActionsHandler { fun onClearAddressButtonClick() { diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt index 4708050380..4b3ac9357d 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt @@ -6,11 +6,14 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.entities.ProgressState +import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.rekotlin.Action /** @@ -19,21 +22,23 @@ import org.rekotlin.Action internal class DemoOnboardingNoteMiddleware : DemoMiddleware { override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean { - val globalState = store.state.globalState val noteState = store.state.onboardingNoteState - when (action) { + return when (action) { is OnboardingNoteAction.Balance.Update -> { val walletManager = if (noteState.walletManager != null) { noteState.walletManager } else { - val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard { + val wmFactory = runBlocking { + store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() + } + val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard { return false } store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager)) walletManager } + val balanceAmount = config.getBalance(walletManager.wallet.blockchain) val loadedBalance = noteState.walletBalance.copy( value = balanceAmount.value!!, @@ -42,6 +47,7 @@ internal class DemoOnboardingNoteMiddleware : DemoMiddleware { error = null, criticalError = null, ) + walletManager.wallet.setAmount(balanceAmount) scope.launch { @@ -53,7 +59,7 @@ internal class DemoOnboardingNoteMiddleware : DemoMiddleware { } return true } + else -> false } - return false } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 05c586372d..fa88770744 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -4,8 +4,6 @@ import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action @@ -26,12 +24,15 @@ sealed class DetailsAction : Action { data object Failure : ResetToFactory() data object Success : ResetToFactory() - data class LastWarningDialogVisibility(val isShown: Boolean) : ResetToFactory() + data class ShowDialog(val dialog: CardSettingsState.Dialog) : ResetToFactory() + + data object DismissDialog : ResetToFactory() } data object ScanCard : DetailsAction() - data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() + data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction() + data object ResetCardSettingsData : DetailsAction() data object ScanAndSaveUserWallet : DetailsAction() { 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 654edd4102..c82a82d087 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 @@ -5,7 +5,6 @@ import com.tangem.common.* import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy -import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction @@ -14,25 +13,25 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.TapWorkarounds.isTangemTwins -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.userwallets.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.tap.* +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R @@ -83,6 +82,8 @@ class DetailsMiddleware { class EraseWalletMiddleware { @Suppress("CyclomaticComplexMethod") fun handle(action: DetailsAction.ResetToFactory) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + when (action) { is DetailsAction.ResetToFactory.Start -> { val card = store.state.detailsState.cardSettingsState?.card ?: return @@ -145,7 +146,7 @@ class DetailsMiddleware { } .doOnFailure { error -> if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) + Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error)) } } .doOnResult { @@ -329,8 +330,10 @@ class DetailsMiddleware { } private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + // Nothing to change - if (preferencesStorage.shouldSaveAccessCodes == enable) { + if (shouldSaveAccessCodes == enable) { store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) return@launch } @@ -368,50 +371,6 @@ class DetailsMiddleware { private suspend fun saveCurrentWallet( scanResponse: ScanResponse?, enableAccessCodesSaving: Boolean, - ): CompletionResult { - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - - return if (featureToggles.isGeneralManagerEnabled) { - saveCurrentWalletByNewWay(scanResponse, enableAccessCodesSaving) - } else { - saveCurrentWalletByOldWay(scanResponse, enableAccessCodesSaving) - } - } - - private suspend fun saveCurrentWalletByOldWay( - scanResponse: ScanResponse?, - enableAccessCodesSaving: Boolean, - ): CompletionResult { - val userWallet = userWalletsListManager.selectedUserWalletSync - ?: scanResponse?.let { UserWalletBuilder(it).build() } - ?: return CompletionResult.Failure( - error = TangemSdkError.ExceptionError(IllegalStateException("scanResponse is null")), - ) - - updateUserWalletsListManager(enableUserWalletsSaving = true) - - return userWalletsListManager.save(userWallet) - .flatMap { - if (enableAccessCodesSaving) { - saveAccessCodes(scanResponse) - } else { - CompletionResult.Success(Unit) - } - } - .doOnSuccess { - Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On)) - - preferencesStorage.shouldShowSaveUserWalletScreen = false - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - } - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - } - } - - private suspend fun saveCurrentWalletByNewWay( - scanResponse: ScanResponse?, - enableAccessCodesSaving: Boolean, ): CompletionResult { store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) @@ -429,31 +388,6 @@ class DetailsMiddleware { } private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult { - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - - return if (featureToggles.isGeneralManagerEnabled) { - deleteSavedWalletsAndAccessCodesByNewWay() - } else { - deleteSavedWalletsAndAccessCodesByOldWay() - } - } - - private suspend fun deleteSavedWalletsAndAccessCodesByOldWay(): CompletionResult { - return userWalletsListManager.clear() - .doOnSuccess { - Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) - deleteSavedAccessCodes() - updateUserWalletsListManager(enableUserWalletsSaving = false) - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) - } - .doOnFailure { error -> - Timber.e(error, "Unable to delete saved wallets") - } - } - - private suspend fun deleteSavedWalletsAndAccessCodesByNewWay(): CompletionResult { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) deleteSavedAccessCodes() @@ -464,12 +398,14 @@ class DetailsMiddleware { return CompletionResult.Success(Unit) } - private fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult { + private suspend fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On)) - preferencesStorage.shouldSaveAccessCodes = true - store.inject(DaggerGraphState::cardSdkConfigRepository) - .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true) + store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true) + + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true, + ) return CompletionResult.Success(Unit) } @@ -479,33 +415,16 @@ class DetailsMiddleware { .doOnSuccess { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off)) - preferencesStorage.shouldSaveAccessCodes = false - store.inject(DaggerGraphState::cardSdkConfigRepository) - .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false) + store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = false) + + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = false, + ) } .doOnFailure { error -> Timber.e(error, "Unable to delete saved access codes") } } - - private suspend fun updateUserWalletsListManager(enableUserWalletsSaving: Boolean) { - val manager = if (enableUserWalletsSaving) { - createBiometricsUserWalletsManager() ?: return - } else { - UserWalletsListManager.provideRuntimeImplementation() - } - - store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) - } - - private fun createBiometricsUserWalletsManager(): UserWalletsListManager? { - val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard { - Timber.e(IllegalStateException("No activities in foreground")) - return null - } - - return UserWalletsListManager.provideBiometricImplementation(context) - } } class AccessCodeRecoveryMiddleware { @@ -552,10 +471,7 @@ class DetailsMiddleware { if (isSameWallet) { store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData( - scanResponse.card, - scanResponse.cardTypesResolver, - ), + DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse), ) } else { store.dispatchDialogShow( @@ -574,9 +490,8 @@ class DetailsMiddleware { val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() // Update access code policy for access code saving when a card was scanned - cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, - ) + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = shouldSaveAccessCodes) store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsSource = CoreAnalyticsParam.ScreensSources.Settings, @@ -588,35 +503,70 @@ class DetailsMiddleware { store.dispatchOnMain(NavigationAction.PopBackTo()) }, onSuccess = { scanResponse -> - saveUserWalletAndPopBackToWalletScreen(scanResponse) + createUserWallet(scanResponse) + .doOnSuccess { + saveUserWalletAndPopBackToWalletScreen( + userWallet = it, + prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode, + ) + } .doOnFailure { error -> - // Rollback policy if card saving was failed - cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) - Timber.e(error, "Unable to save user wallet") - - store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + Timber.e(error, "Unable to create user wallet") + handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode) } }, onFailure = { error -> - // Rollback policy if card scanning was failed - cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) Timber.e(error, "Unable to scan card") - store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode) }, ) } - private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult { - val userWallet = UserWalletBuilder(scanResponse).build() - ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + private suspend fun createUserWallet(scanResponse: ScanResponse): CompletionResult { + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build() - return userWalletsListManager.save(userWallet) + return if (userWallet != null) { + CompletionResult.Success(userWallet) + } else { + CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + } + } + + private suspend fun saveUserWalletAndPopBackToWalletScreen( + userWallet: UserWallet, + prevUseBiometricsForAccessCode: Boolean, + ) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + + userWalletsListManager.save(userWallet) .doOnSuccess { store.onUserWalletSelected(userWallet) store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) } + .doOnFailure { error -> + if (error is UserWalletsListError.WalletAlreadySaved) { + userWalletsListManager.select(userWallet.walletId) + store.onUserWalletSelected(userWallet) + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + } else { + Timber.e(error, "Unable to create user wallet") + handleError(error, prevUseBiometricsForAccessCode) + } + } + } + + private suspend fun handleError(error: TangemError, prevUseBiometricsForAccessCode: Boolean) { + val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository) + + // Rollback policy if card saving was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) } private fun TangemError.toTextReference(): TextReference? { 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 5b3f2fafd3..5a915b21d5 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 @@ -1,13 +1,14 @@ package com.tangem.tap.features.details.redux +import com.tangem.core.navigation.AppScreen import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.extensions.signedHashesCount -import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -26,14 +27,10 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { val detailsState = state.detailsState return when (action) { is DetailsAction.PrepareScreen -> { - handlePrepareScreen(action) + handlePrepareScreen(action, state) } is DetailsAction.PrepareCardSettingsData -> { - handlePrepareCardSettingsScreen( - card = action.card, - cardTypesResolver = action.cardTypesResolver, - state = detailsState, - ) + handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState) } is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null) is DetailsAction.ResetToFactory -> { @@ -68,14 +65,22 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { } } -private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState { +private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState { return DetailsState( scanResponse = action.scanResponse, + // If the current screen is ResetToFactory, we must save the ResetToFactory's state + cardSettingsState = if (store.state.navigationState.backStack.lastOrNull() == AppScreen.ResetToFactory) { + state.detailsState.cardSettingsState + } else { + null + }, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, appSettingsState = AppSettingsState( isBiometricsAvailable = tangemSdkManager.canUseBiometry, saveWallets = action.shouldSaveUserWallets, - saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, + saveAccessCodes = runBlocking { + store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + }, selectedAppCurrency = store.state.globalState.appCurrency, selectedThemeMode = runBlocking { store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() @@ -89,17 +94,17 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta ) } -private fun handlePrepareCardSettingsScreen( - card: CardDTO, - cardTypesResolver: CardTypesResolver, - state: DetailsState, -): DetailsState { +private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState { + val cardTypesResolver = scanResponse.cardTypesResolver + val card = scanResponse.card val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active + val cardSettingsState = CardSettingsState( cardInfo = card.toCardInfo(cardTypesResolver), + scanResponse = scanResponse, manageSecurityState = prepareSecurityOptions(card, cardTypesResolver), - card = card, + card = scanResponse.card, resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver), resetButtonEnabled = false, condition1Checked = false, @@ -114,8 +119,9 @@ private fun handlePrepareCardSettingsScreen( null }, isShowPasswordResetRadioButton = isShowPasswordResetRadioButton, - isLastWarningDialogShown = false, + dialog = null, ) + return state.copy(cardSettingsState = cardSettingsState) } @@ -182,14 +188,12 @@ private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: Detai ), ) } - is DetailsAction.ResetToFactory.LastWarningDialogVisibility -> { - state.copy( - cardSettingsState = cardSettingsState?.copy( - isLastWarningDialogShown = action.isShown, - ), - ) + is DetailsAction.ResetToFactory.ShowDialog -> { + state.copy(cardSettingsState = cardSettingsState?.copy(dialog = action.dialog)) + } + is DetailsAction.ResetToFactory.DismissDialog -> { + state.copy(cardSettingsState = cardSettingsState?.copy(dialog = null)) } - else -> state } } 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 e1c523e759..8e7e583ecf 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 @@ -39,6 +39,7 @@ data class AccessCodeRecoveryState( data class CardSettingsState( val cardInfo: CardInfo, val card: CardDTO, + val scanResponse: ScanResponse, val manageSecurityState: ManageSecurityState?, val resetCardAllowed: Boolean, val resetButtonEnabled: Boolean, @@ -46,8 +47,16 @@ data class CardSettingsState( val condition2Checked: Boolean, val accessCodeRecovery: AccessCodeRecoveryState? = null, val isShowPasswordResetRadioButton: Boolean, - val isLastWarningDialogShown: Boolean, -) + val dialog: Dialog?, +) { + + sealed interface Dialog { + data object StartResetDialog : Dialog + data object ContinueResetDialog : Dialog + data object InterruptedResetDialog : Dialog + data object CompletedResetDialog : Dialog + } +} data class ManageSecurityState( val currentOption: SecurityOption = SecurityOption.LongTap, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 47afd1b484..e96974c3b4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -1,118 +1,26 @@ package com.tangem.tap.features.details.redux.walletconnect -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.walletconnect.Topic import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen -import com.tangem.wallet.R -import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage -import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction -import com.trustwallet.walletconnect.models.session.WCSession import org.rekotlin.Action sealed class WalletConnectAction : Action { - object ResetState : WalletConnectAction() data class HandleDeepLink(val wcUri: String?) : WalletConnectAction() - data class RestoreSessions(val scanResponse: ScanResponse) : WalletConnectAction() + data class StartWalletConnect( val copiedUri: String?, ) : WalletConnectAction() - data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() - object UnsupportedCard : WalletConnectAction() data class OpenSession( val wcUri: String, ) : WalletConnectAction() - data class SetNewSessionData( - val newSession: NewWcSessionData, - ) : WalletConnectAction() + data class DisconnectSession(val topic: String) : WalletConnectAction() - object RefuseOpeningSession : WalletConnectAction() - data class OpeningSessionTimeout(val session: WCSession) : WalletConnectAction() - data class ScanCard( - val session: WalletConnectSession, - val chainId: Int?, - ) : WalletConnectAction() - - data class ApproveSession( - val session: WCSession, - ) : WalletConnectAction() { - data class Success(val session: WalletConnectSession) : WalletConnectAction() - } - - data class SwitchBlockchain( - val blockchain: Blockchain?, - val session: WalletConnectSession, - ) : WalletConnectAction() - - data class SelectNetwork(val session: WalletConnectSession, val networks: List) : WalletConnectAction() - data class ChooseNetwork(val blockchain: Blockchain) : WalletConnectAction() - data class UpdateBlockchain( - val updatedSession: WalletConnectSession, - ) : WalletConnectAction() - - data class FailureEstablishingSession(val session: WCSession?, val error: TapError? = null) : WalletConnectAction() - data class SetSessionsRestored(val sessions: List) : WalletConnectAction() - - data class DisconnectSession(val topic: String, val session: WCSession?) : WalletConnectAction() - - data class RemoveSession(val session: WCSession) : WalletConnectAction() - - data class HandleTransactionRequest( - val transaction: WCEthereumTransaction, - val session: WalletConnectSession, - val id: Long, - val type: WcEthTransactionType, - ) : - WalletConnectAction() - - data class HandlePersonalSignRequest( - val message: WCEthereumSignMessage, - val session: WalletConnectSession, - val id: Long, - ) : WalletConnectAction() - - data class SendTransaction(val topic: Topic) : WalletConnectAction() - - data class SignMessage(val topic: Topic) : WalletConnectAction() - - data class RejectRequest(val topic: Topic, val id: Long) : WalletConnectAction() - - object NotEnoughFunds : WalletConnectAction(), NotificationAction { - override val messageResource = R.string.wallet_connect_create_tx_not_enough_funds - } - - object NotifyCameraPermissionIsRequired : WalletConnectAction(), NotificationAction { - override val messageResource = R.string.common_camera_denied_alert_message - } - - object BinanceTransaction : WalletConnectAction() { - data class Trade( - val id: Long, - val order: WCBinanceTradeOrder, - val sessionData: WalletConnectSession, - ) : WalletConnectAction() - - data class Transfer( - val id: Long, - val order: WCBinanceTransferOrder, - val sessionData: WalletConnectSession, - ) : WalletConnectAction() - - data class Sign( - val id: Long, - val data: ByteArray, - val topic: Topic, - ) : WalletConnectAction() - } + data class RejectRequest(val topic: String, val id: Long) : WalletConnectAction() + data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() //region WalletConnect 2.0 object ApproveProposal : WalletConnectAction() object RejectProposal : WalletConnectAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 4a26922744..b4917ceabb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -1,44 +1,27 @@ package com.tangem.tap.features.details.redux.walletconnect import androidx.core.os.bundleOf -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.extensions.guard +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.walletconnect.WalletConnectActions -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect.BnbHelper -import com.tangem.tap.domain.walletconnect.WalletConnectManager -import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils -import com.tangem.tap.domain.walletconnect.extensions.toWcEthTransaction import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.Account -import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager -import kotlinx.coroutines.Dispatchers +import com.tangem.wallet.R import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -46,10 +29,9 @@ import timber.log.Timber @Suppress("LargeClass") class WalletConnectMiddleware { - private var walletConnectManager = WalletConnectManager() private val walletConnectInteractor: WalletConnectInteractor get() = store.inject(DaggerGraphState::walletConnectInteractor) - private val walletConnectRepository: WalletConnectRepository + private val walletConnectRepository: LegacyWalletConnectRepository get() = store.inject(DaggerGraphState::walletConnectRepository) val walletConnectMiddleware: Middleware = { dispatch, state -> @@ -66,40 +48,18 @@ class WalletConnectMiddleware { if (DemoHelper.tryHandle(state, action)) return when (action) { - is WalletConnectActions.New.Initialize -> { - val userWallet = action.userWallet - val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) { - userWallet.cardId - } else { // if wallet has backup, any card from wallet can be used to sign - null - } - scope.launch { - val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch - wcInteractor.startListening( - userWalletId = userWallet.walletId.stringValue, - cardId = cardId, - ) - } - } - is WalletConnectActions.New.SetupUserChains -> { - scope.launch { - val userWallet = action.userWallet - val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch - wcInteractor.setUserChains(getAccountsForWc(wcInteractor, userWallet)) - } - } - is WalletConnectAction.ResetState -> walletConnectManager = WalletConnectManager() - is WalletConnectAction.RestoreSessions -> { - walletConnectManager.restoreSessions(action.scanResponse) - } is WalletConnectAction.HandleDeepLink -> { if (!action.wcUri.isNullOrBlank()) { store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri)) } } + is WalletConnectAction.DisconnectSession -> { + walletConnectInteractor.disconnectSession(action.topic) + } is WalletConnectAction.StartWalletConnect -> { val uri = action.copiedUri if (uri != null && isWalletConnectUri(uri)) { + // TODO check store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { store.dispatchOnMain( @@ -112,27 +72,6 @@ class WalletConnectMiddleware { ) } } - is WalletConnectAction.SelectNetwork -> { - store.dispatch( - GlobalAction.ShowDialog( - WalletConnectDialog.ChooseNetwork( - session = action.session, - networks = action.networks, - ), - ), - ) - } - is WalletConnectAction.ChooseNetwork -> { - val data = state()?.walletConnectState?.newSessionData ?: return - scope.launch { - prepareWalletManager( - scanResponse = data.scanResponse, - blockchain = action.blockchain, - session = data.session, - walletConnectManager = walletConnectManager, - ) - } - } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { store.dispatchOnMain( GlobalAction.ShowDialog( @@ -142,176 +81,15 @@ class WalletConnectMiddleware { ), ) } - is WalletConnectAction.OpeningSessionTimeout -> { - Timber.e("OpeningSessionTimeout for topic ${action.session.topic}") - // do not show dialog for now, it shows always to user if cannot establish connection - // store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout)) - } - is WalletConnectAction.FailureEstablishingSession -> { - Timber.e("FailureEstablishingSession for topic ${action.session?.topic}") - // disable alerts in release to avoid annoying users - // if (action.error != null) { - // store.dispatch( - // GlobalAction.ShowDialog( - // AppDialog.SimpleOkDialogRes( - // headerId = R.string.common_warning, - // messageId = action.error.messageResource, - // ), - // ), - // ) - // } - if (action.session != null) { - walletConnectManager.disconnect(action.session) - } - } - is WalletConnectAction.UnsupportedCard -> { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard)) - } is WalletConnectAction.OpenSession -> { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { - '1' -> { - walletConnectManager.connect(wcUri = action.wcUri) - } '2' -> walletConnectRepository.pair(uri = action.wcUri) } } - is WalletConnectAction.RefuseOpeningSession -> { - Timber.e("RefuseOpeningSession") - - // do not show for now to avoid anoying users with alert - // store.dispatch( - // GlobalAction.ShowDialog( - // WalletConnectDialog.OpeningSessionRejected, - // ), - // ) - } - is WalletConnectAction.ScanCard -> { - val scanResponse = userWalletsListManager.selectedUserWalletSync.guard { - Timber.w("Unable to get selected user wallet for WC session") - return - } - - scope.launch(Dispatchers.Main) { - scanCard(scanResponse, action.session, action.chainId) - } - } - is WalletConnectAction.ApproveSession -> { - walletConnectManager.approve(action.session) - } - is WalletConnectAction.DisconnectSession -> { - if (action.session != null) { - walletConnectManager.disconnect(action.session) - } else { - walletConnectInteractor.disconnectSession(action.topic) - } - } - is WalletConnectAction.HandleTransactionRequest -> { - walletConnectManager.handleTransactionRequest( - transaction = action.transaction.toWcEthTransaction(), - session = action.session, - id = action.id, - type = action.type, - ) - } - is WalletConnectAction.HandlePersonalSignRequest -> { - walletConnectManager.handlePersonalSignRequest( - message = action.message, - session = action.session, - id = action.id, - ) - } is WalletConnectAction.RejectRequest -> { - walletConnectManager.rejectRequest(action.topic, action.id) walletConnectInteractor.cancelRequest(action.topic, action.id) } - is WalletConnectAction.SendTransaction -> { - walletConnectManager.completeTransaction(action.topic) - } - is WalletConnectAction.SignMessage -> { - walletConnectManager.sendSignedMessage(action.topic) - } - is WalletConnectAction.BinanceTransaction.Trade -> { - val messageData = BnbHelper.createMessageData(action.order) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.BnbTransactionDialog( - WcPreparedRequest.BnbTransaction( - preparedRequestData = BnbData( - data = messageData, - topic = action.sessionData.session.topic, - requestId = action.id, - dAppName = action.sessionData.peerMeta.name, - ), - topic = action.sessionData.session.topic, - requestId = action.id, - derivationPath = action.sessionData.wallet.derivationPath?.rawPath, - ), - ), - ), - ) - } - is WalletConnectAction.BinanceTransaction.Transfer -> { - val messageData = BnbHelper.createMessageData(action.order) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.BnbTransactionDialog( - WcPreparedRequest.BnbTransaction( - preparedRequestData = BnbData( - data = messageData, - topic = action.sessionData.session.topic, - requestId = action.id, - dAppName = action.sessionData.peerMeta.name, - ), - topic = action.sessionData.session.topic, - requestId = action.id, - derivationPath = action.sessionData.wallet.derivationPath?.rawPath, - ), - ), - ), - ) - } - is WalletConnectAction.BinanceTransaction.Sign -> { - walletConnectManager.signBnb( - id = action.id, - data = action.data, - topic = action.topic, - ) - } - is WalletConnectAction.SwitchBlockchain -> { - if (action.session.wallet.derivationStyle == DerivationStyle.LEGACY) { - Timber.d("Cannot switch chains on AC01/AC02 wallets") - return - } - val blockchain = action.blockchain.guard { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) - return - } - scope.launch { - val walletManager = getWalletManager( - wallet = action.session.wallet, - blockchain = blockchain, - ).guard { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)), - ), - ) - return@launch - } - val updatedWallet = action.session.wallet.copy( - walletPublicKey = walletManager.wallet.publicKey.seedKey, - derivedPublicKey = walletManager.wallet.publicKey.derivedKey, - derivationPath = walletManager.wallet.publicKey.derivationPath, - blockchain = action.blockchain, - ) - val updatedSession = action.session.copy(wallet = updatedWallet) - store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) - } - } - is WalletConnectAction.UpdateBlockchain -> { - walletConnectManager.updateBlockchain(action.updatedSession) - } is WalletConnectAction.ApproveProposal -> { scope.launch { val accounts = getWalletManagers() @@ -359,6 +137,17 @@ class WalletConnectMiddleware { ), ) } + is WalletConnectError.UnknownError -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + AppDialog.SimpleOkDialogRes( + headerId = R.string.wallet_connect_title, + messageId = R.string.wallet_connect_error_with_framework_message, + args = listOf(action.error.message), + ), + ), + ) + } is WalletConnectError.ExternalApprovalError -> { Timber.e(action.error, "ExternalApprovalError ${action.error.message}") // do not show dialog on this event @@ -397,140 +186,13 @@ class WalletConnectMiddleware { private suspend fun getWalletManagers(): List { val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() return walletManagerFacade.getStoredWalletManagers(userWallet.walletId) } - private suspend fun scanCard(userWallet: UserWallet, session: WalletConnectSession, chainId: Int?) { - val blockchain = WalletConnectNetworkUtils.parseBlockchain( - chainId = chainId, - peer = session.peerMeta, - ).guard { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) - return - } - - handleScanResponse(userWallet, session, blockchain) - } - - private suspend fun getAvailableEvmBlockchains(userWalletId: UserWalletId): List { - val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) - - return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - .asSequence() - .filterIsInstance() - .filterNot { it.isCustom } - .mapNotNull { Blockchain.fromNetworkId(it.network.id.value) } - .filter { it.isEvm() } - .toList() - } - - private suspend fun prepareWalletManager( - scanResponse: ScanResponse, - blockchain: Blockchain, - session: WalletConnectSession, - walletConnectManager: WalletConnectManager, - ) { - val walletManager = getWalletManager(session.wallet, blockchain).guard { - store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session)) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)), - ), - ) - return - } - val wallet = walletManager.wallet - val derivedKey = - if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) { - null - } else { - walletManager.wallet.publicKey.blockchainKey - } - val walletForSession = WalletForSession( - walletPublicKey = wallet.publicKey.seedKey, - derivedPublicKey = derivedKey, - derivationPath = wallet.publicKey.derivationPath, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - blockchain = wallet.blockchain, - ) - - withMainContext { - val updatedSession = session.copy(wallet = walletForSession) - walletConnectManager.updateSession(updatedSession) - - store.dispatch(WalletConnectAction.ApproveSession(session.session)) - } - } - - private suspend fun handleScanResponse( - userWallet: UserWallet, - session: WalletConnectSession, - blockchain: Blockchain, - ) { - val scanResponse = userWallet.scanResponse - - if (!scanResponse.cardTypesResolver.isMultiwalletAllowed()) { - store.dispatchOnMain(WalletConnectAction.UnsupportedCard) - return - } - val updatedSession = session.copy(wallet = session.wallet.copy(blockchain = blockchain)) - store.dispatch( - WalletConnectAction.SetNewSessionData( - NewWcSessionData(updatedSession, scanResponse, blockchain), - ), - ) - val blockchains = if (blockchain.isEvm()) { - getAvailableEvmBlockchains(userWallet.walletId) - } else { - emptyList() - } - store.dispatch( - GlobalAction.ShowDialog( - WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains), - ), - ) - } - - private suspend fun getWalletManager(wallet: WalletForSession, blockchain: Blockchain): WalletManager? { - val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) { - Blockchain.EthereumTestnet - } else { - blockchain - } - val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null - val derivation = blockchainToMake.derivationPath( - style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), - )?.rawPath - - val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - - return walletManagerFacade.getOrCreateWalletManager( - userWalletId = userWallet.walletId, - blockchain = blockchainToMake, - derivationPath = derivation, - ) - } - private fun isWalletConnectUri(uri: String): Boolean { - return WalletConnectManager.isCorrectWcUri(uri) || walletConnectInteractor.isWalletConnectUri(uri) - } - - private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor, userWallet: UserWallet): List { - val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - return walletManagerFacade.getStoredWalletManagers(userWallet.walletId).mapNotNull { - val wallet = it.wallet - val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), - ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } - } + return walletConnectInteractor.isWalletConnectUri(uri) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index 6840f26c63..20217386f2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -7,36 +7,9 @@ object WalletConnectReducer { if (action !is WalletConnectAction) return state return when (action) { - is WalletConnectAction.ResetState -> return WalletConnectState() - is WalletConnectAction.ApproveSession.Success -> { - state.copy( - loading = false, - sessions = state.sessions + action.session, - ) - } is WalletConnectAction.OpenSession -> { state.copy(loading = true) } - is WalletConnectAction.SetNewSessionData -> { - state.copy(newSessionData = action.newSession) - } - is WalletConnectAction.SetSessionsRestored -> state.copy( - sessions = action.sessions, - ) - is WalletConnectAction.RemoveSession -> { - val sessions = - state.sessions.filterNot { it.session.toUri() == action.session.toUri() } - state.copy(sessions = sessions) - } - is WalletConnectAction.UnsupportedCard, - is WalletConnectAction.RefuseOpeningSession, - is WalletConnectAction.OpeningSessionTimeout, - is WalletConnectAction.FailureEstablishingSession, - -> state.copy(loading = false) - is WalletConnectAction.UpdateBlockchain -> state.copy( - sessions = state.sessions - .filterNot { it.peerId == action.updatedSession.peerId } + action.updatedSession, - ) is WalletConnectAction.ApproveProposal -> state.copy(loading = true) is WalletConnectAction.RejectProposal, is WalletConnectAction.SessionEstablished, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 68cfd4255b..750f4b04c5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -14,12 +14,9 @@ import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.session.WCSession data class WalletConnectState( val loading: Boolean = false, - val sessions: List = listOf(), val wc2Sessions: List = listOf(), val newSessionData: NewWcSessionData? = null, ) @@ -34,8 +31,6 @@ data class WalletConnectSession( val peerId: String, val remotePeerId: String?, val wallet: WalletForSession, - val session: WCSession, - val peerMeta: WCPeerMeta, ) { fun getAddress(): String? { val key = wallet.derivedPublicKey ?: wallet.walletPublicKey ?: return null diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt index b9273ef99b..d48050ec85 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -16,7 +16,7 @@ import javax.inject.Inject internal class AppCurrencySelectorFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: AppCurrencySelectorViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt index a38cac5c0a..b29b503a39 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appcurrency +import android.content.res.Configuration import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -25,6 +26,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency import com.tangem.wallet.R @@ -302,21 +304,12 @@ private val RadioButtonColors: RadioButtonColors // region Preview @Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AppCurrencySelectorScreenPreview_Light( +private fun AppCurrencySelectorScreenPreview( @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, ) { - TangemTheme(isDark = false) { - AppCurrencySelectorScreen(param) - } -} - -@Preview(showBackground = true, widthDp = 360, heightDp = 720) -@Composable -private fun AppCurrencySelectorScreenPreview_Dark( - @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { AppCurrencySelectorScreen(param) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 61e02daeda..2efc912cca 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store @@ -19,7 +19,7 @@ import javax.inject.Inject internal class AppSettingsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var appCurrencyRepository: AppCurrencyRepository diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 7c99ed8b24..c60f34ecce 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings +import android.content.res.Configuration import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -10,6 +11,7 @@ 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.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -70,21 +72,12 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AppSettingsScreenPreview_Light( +private fun AppSettingsScreenPreview( @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, ) { - TangemTheme { - AppSettingsScreen(state = state, onBackClick = {}) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreenPreview_Dark( - @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { AppSettingsScreen(state = state, onBackClick = {}) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index 1dce42e622..b7b5b15da2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -8,7 +9,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -34,17 +35,10 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { - TangemTheme { - SettingsAlertDialog(dialog = dialog) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { - TangemTheme(isDark = true) { +private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { + TangemThemePreview { SettingsAlertDialog(dialog = dialog) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt index 946c62d2f2..17dccc7a63 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -52,17 +54,10 @@ internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { - TangemTheme { - SettingsButtonItem(item = item) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { - TangemTheme(isDark = true) { +private fun ButtonItemPreview(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemThemePreview { SettingsButtonItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt index 1560b14a43..1e43c8f328 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon @@ -16,6 +17,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW16 import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -59,17 +61,10 @@ internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) { - TangemTheme { - SettingsCardItem(item = item) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) { - TangemTheme(isDark = true) { +private fun CardItemPreview(@PreviewParameter(CardItemProvider::class) item: Item.Card) { + TangemThemePreview { SettingsCardItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt index 8c1a93e307..41b568b7ed 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -8,7 +9,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -31,17 +32,10 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { - TangemTheme(isDark = false) { - SettingsSelectorDialog(param) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { - TangemTheme(isDark = true) { +private fun SettingsSelectorDialogPreview(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemThemePreview { SettingsSelectorDialog(param) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index dfbc486b5c..a8476e3bd2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW32 import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -71,17 +73,10 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { - TangemTheme { - SettingsSwitchItem(item = item) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { - TangemTheme(isDark = true) { +private fun SwitchItemPreview(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemThemePreview { SettingsSwitchItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 82431ec211..c81e2d819e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.viewModels import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -16,7 +16,7 @@ import javax.inject.Inject internal class CardSettingsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: CardSettingsViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index b528123bb7..b0e2785b32 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.cardsettings +import android.content.res.Configuration import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -12,6 +13,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold @@ -182,17 +184,10 @@ private fun CardSettingsScreenStateSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun CardSettingsScreenStatePreview_Light() { - TangemTheme { - CardSettingsScreenStateSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun CardSettingsScreenStatePreview_Dark() { - TangemTheme(isDark = true) { +private fun CardSettingsScreenStatePreview() { + TangemThemePreview { CardSettingsScreenStateSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt index 3fa69eebb7..7a365126b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +22,7 @@ class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var walletsRepository: WalletsRepository @Inject - lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles - - @Inject - lateinit var getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase - - @Inject - lateinit var emailSender: EmailSender + lateinit var userWalletsListManager: UserWalletsListManager private lateinit var detailsViewModel: DetailsViewModel @@ -43,9 +35,7 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber Unit, ) : SettingsItem( - iconResId = R.drawable.ic_walletconnect, + iconResId = R.drawable.ic_wallet_connect_24, title = resourceReference(R.string.wallet_connect_title), subtitle = resourceReference(R.string.wallet_connect_subtitle), isLarge = true, @@ -81,14 +81,14 @@ internal sealed class SettingsItem( data class Chat( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_chat, + iconResId = R.drawable.ic_chat_24, title = resourceReference(R.string.details_chat), ) data class SendFeedback( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_comment, + iconResId = R.drawable.ic_comment_24, title = resourceReference(R.string.details_row_title_contact_to_support), ) @@ -102,7 +102,7 @@ internal sealed class SettingsItem( data class TermsOfService( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_text, + iconResId = R.drawable.ic_text_24, title = resourceReference(R.string.disclaimer_title), ) @@ -125,15 +125,15 @@ internal sealed class EventError { } sealed class SocialNetwork(val id: String, val iconRes: Int) { - object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter) - object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) - object Discord : SocialNetwork("Discord", R.drawable.ic_discord) - object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit) - object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram) - object GitHub : SocialNetwork("GitHub", R.drawable.ic_github) - object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook) - object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin) - object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube) + object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter_24) + object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram_24) + object Discord : SocialNetwork("Discord", R.drawable.ic_discord_24) + object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit_24) + object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram_24) + object GitHub : SocialNetwork("GitHub", R.drawable.ic_github_24) + object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook_24) + object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin_24) + object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube_24) } internal object TangemSocialAccounts { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 1819ad46c5..95a6865b3c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -8,14 +8,12 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.feedback.FeedbackManagerFeatureToggles -import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.addContext @@ -29,9 +27,7 @@ import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE -import com.tangem.tap.mainScope import com.tangem.tap.scope -import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -41,7 +37,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import org.rekotlin.Store import timber.log.Timber @@ -49,9 +44,7 @@ import timber.log.Timber internal class DetailsViewModel( private val store: Store, private val walletsRepository: WalletsRepository, - private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, - private val getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase, - private val emailSender: EmailSender, + private val userWalletsListManager: UserWalletsListManager, ) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) @@ -148,21 +141,7 @@ internal class DetailsViewModel( private fun sendFeedback() { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings)) - if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { - mainScope.launch { - val email = getSupportFeedbackEmailUseCase() - emailSender.send( - email = EmailSender.Email( - address = email.address, - subject = email.subject, - message = email.message, - attachment = email.file, - ), - ) - } - } else { - store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) - } + store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) } private fun navigateToAppSettings() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 5414ff05c6..74424cd8ba 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -1,15 +1,16 @@ package com.tangem.tap.features.details.ui.resetcard import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.MutableStateFlow import org.rekotlin.StoreSubscriber import javax.inject.Inject @@ -17,17 +18,18 @@ import javax.inject.Inject internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies - private val viewModel = ResetCardViewModel(store) + private val viewModel: ResetCardViewModel by viewModels() - private var screenState: MutableState = - mutableStateOf(ResetCardScreenState.InitialState) + private var screenState = MutableStateFlow(ResetCardScreenState.InitialState) @Composable override fun ScreenContent(modifier: Modifier) { + val state = screenState.collectAsStateWithLifecycle().value + ResetCardScreen( - state = screenState.value, + state = state, onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, modifier = modifier, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 35b4c9bae8..85bec44728 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,23 +1,26 @@ package com.tangem.tap.features.details.ui.resetcard +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.IconToggleButton -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconToggleButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R +import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.ResetCardScreenContent.Dialog as ResetCardDialog @Composable internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { @@ -34,7 +37,14 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni onBackClick = onBackClick, ) - LastWarningDialog(state = state) + when (val dialog = (state as? ResetCardScreenState.ResetCardScreenContent)?.dialog) { + is ResetCardDialog.StartReset, + is ResetCardDialog.ContinueReset, + is ResetCardDialog.InterruptedReset, + -> CommonResetDialog(dialog = dialog) + is ResetCardDialog.CompletedReset -> CompletedResetDialog(dialog = dialog) + null -> Unit + } } @Composable @@ -182,23 +192,35 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { } @Composable -private fun LastWarningDialog(state: ResetCardScreenState) { - if (state is ResetCardScreenState.ResetCardScreenContent && state.lastWarningDialog.isShown) { - BasicDialog( - title = stringResource(id = R.string.common_attention), - message = stringResource(id = R.string.card_settings_action_sheet_title), - dismissButton = DialogButton( - title = stringResource(id = R.string.card_settings_action_sheet_reset), - warning = true, - onClick = state.lastWarningDialog.onResetButtonClick, - ), - confirmButton = DialogButton( - title = stringResource(id = R.string.common_cancel), - onClick = state.lastWarningDialog.onDismiss, - ), - onDismissDialog = state.lastWarningDialog.onDismiss, - ) - } +private fun CommonResetDialog(dialog: ResetCardScreenState.ResetCardScreenContent.Dialog) { + BasicDialog( + title = stringResource(dialog.titleResId), + message = stringResource(dialog.messageResId), + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = dialog.onDismiss, + ), + confirmButton = DialogButton( + title = stringResource(id = R.string.card_settings_action_sheet_reset), + warning = true, + onClick = dialog.onConfirmClick, + ), + onDismissDialog = dialog.onDismiss, + ) +} + +@Composable +private fun CompletedResetDialog(dialog: ResetCardDialog) { + BasicDialog( + title = stringResource(id = dialog.titleResId), + message = stringResource(id = dialog.messageResId), + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + onClick = dialog.onConfirmClick, + ), + onDismissDialog = {}, + isDismissable = false, + ) } // region Preview @@ -216,11 +238,7 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { onAcceptCondition1ToggleClick = {}, onAcceptCondition2ToggleClick = {}, onResetButtonClick = {}, - lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog( - isShown = false, - onResetButtonClick = {}, - onDismiss = {}, - ), + dialog = null, ), onBackClick = {}, ) @@ -228,17 +246,10 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ResetCardScreenPreview_Light() { - TangemTheme { - ResetCardScreenSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun ResetCardScreenPreview_Dark() { - TangemTheme(isDark = true) { +private fun ResetCardScreenPreview() { + TangemThemePreview { ResetCardScreenSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index d70071ed49..0bcff503bc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.details.ui.resetcard +import androidx.annotation.StringRes import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.wallet.R internal sealed class ResetCardScreenState { @@ -15,16 +17,51 @@ internal sealed class ResetCardScreenState { val onAcceptCondition1ToggleClick: (Boolean) -> Unit, val onAcceptCondition2ToggleClick: (Boolean) -> Unit, val onResetButtonClick: () -> Unit, - val lastWarningDialog: LastWarningDialog, + val dialog: Dialog? = null, ) : ResetCardScreenState() { val resetButtonEnabled: Boolean get() = accepted - data class LastWarningDialog( - val isShown: Boolean, - val onResetButtonClick: () -> Unit, - val onDismiss: () -> Unit, - ) + sealed class Dialog( + @StringRes val titleResId: Int, + @StringRes val messageResId: Int, + ) { + + abstract val onConfirmClick: () -> Unit + abstract val onDismiss: () -> Unit + + data class StartReset( + override val onConfirmClick: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog( + titleResId = R.string.common_attention, + messageResId = R.string.card_settings_action_sheet_title, + ) + + data class ContinueReset( + override val onConfirmClick: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog( + titleResId = R.string.card_settings_continue_reset_alert_title, + messageResId = R.string.card_settings_continue_reset_alert_message, + ) + + data class InterruptedReset( + override val onConfirmClick: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog( + titleResId = R.string.card_settings_interrupted_reset_alert_title, + messageResId = R.string.card_settings_interrupted_reset_alert_message, + ) + + data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog( + titleResId = R.string.card_settings_completed_reset_alert_title, + messageResId = R.string.card_settings_completed_reset_alert_message, + ) { + + override val onDismiss: () -> Unit = {} + } + } } internal enum class WarningsToReset { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 02e00914e1..fd32427821 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -1,13 +1,55 @@ package com.tangem.tap.features.details.ui.resetcard -import com.tangem.tap.common.redux.AppState +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.features.details.redux.CardSettingsState -import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText -import org.rekotlin.Store +import com.tangem.tap.store +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject +import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog -internal class ResetCardViewModel(private val store: Store) { +@Suppress("LongParameterList") +@HiltViewModel +internal class ResetCardViewModel @Inject constructor( + private val resetCardFeatureToggles: ResetCardFeatureToggles, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val resetCardUseCase: ResetCardUseCase, + private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val userWalletsListManager: UserWalletsListManager, + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel() { + + private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse + ?: error("ScanResponse can't be null") + + private val currentUserWalletId = createUserWalletId(firstCardScanResponse) + + // TODO: move logic to separate domain entity + private var resetBackupCardCount = 0 fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent { val descriptionText = state?.cardInfo @@ -28,27 +70,158 @@ internal class ResetCardViewModel(private val store: Store) { warningsToShow = warningsToShow, acceptCondition1Checked = state?.condition1Checked ?: false, acceptCondition2Checked = state?.condition2Checked ?: false, - onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) }, - onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) }, - onResetButtonClick = { showLastWarningDialog() }, - lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog( - isShown = state?.isLastWarningDialogShown ?: false, - onResetButtonClick = ::onLastWarningDialogResetClicked, - onDismiss = ::onLastWarningDialogDismiss, - ), + onAcceptCondition1ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition1(it)) }, + onAcceptCondition2ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition2(it)) }, + onResetButtonClick = { showDialog(CardSettingsDialog.StartResetDialog) }, + dialog = state?.dialog?.let(::createDialog), ) } - private fun showLastWarningDialog() { - store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = true)) + private fun createDialog(dialog: CardSettingsDialog): ResetCardScreenState.ResetCardScreenContent.Dialog { + return when (dialog) { + CardSettingsDialog.StartResetDialog -> { + ResetCardScreenState.ResetCardScreenContent.Dialog.StartReset( + onConfirmClick = ::onStartResetClick, + onDismiss = ::dismissDialog, + ) + } + CardSettingsDialog.ContinueResetDialog -> { + ResetCardScreenState.ResetCardScreenContent.Dialog.ContinueReset( + onConfirmClick = ::onContinueResetClick, + onDismiss = ::onContinueResetDialogDismiss, + ) + } + CardSettingsDialog.InterruptedResetDialog -> { + ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset( + onConfirmClick = ::onContinueResetClick, + onDismiss = ::onInterruptedResetDialogDismiss, + ) + } + CardSettingsDialog.CompletedResetDialog -> { + ResetCardScreenState.ResetCardScreenContent.Dialog.CompletedReset( + onConfirmClick = ::dismissAndFinishFullReset, + ) + } + } } - private fun onLastWarningDialogResetClicked() { - store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false)) - store.dispatch(DetailsAction.ResetToFactory.Proceed) + private fun onStartResetClick() { + dismissDialog() + + if (resetCardFeatureToggles.isFullResetEnabled) { + makeFullReset() + } else { + store.dispatch(ResetToFactory.Proceed) + } } - private fun onLastWarningDialogDismiss() { - store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false)) + private fun makeFullReset() { + viewModelScope.launch { + resetCardUseCase(card = firstCardScanResponse.card).onRight { + deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId) + deleteWalletUseCase(currentUserWalletId) + + val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull() + if (newSelectedWallet != null) { + store.onUserWalletSelected(newSelectedWallet) + } + + delay(DELAY_SDK_DIALOG_CLOSE) + + checkRemainingBackupCards() + } + } + } + + private fun onContinueResetClick() { + dismissDialog() + + viewModelScope.launch { + resetCardUseCase( + cardNumber = resetBackupCardCount + 1, + card = firstCardScanResponse.card, + userWalletId = currentUserWalletId, + ) + .onRight { + resetBackupCardCount++ + + delay(DELAY_SDK_DIALOG_CLOSE) + + checkRemainingBackupCards() + } + .onLeft { showDialog(CardSettingsDialog.InterruptedResetDialog) } + } + } + + private fun onContinueResetDialogDismiss() { + dismissDialog() + + showDialog(CardSettingsDialog.InterruptedResetDialog) + } + + private fun onInterruptedResetDialogDismiss() { + analyticsEventHandler.send(Settings.CardSettings.FactoryResetCanceled(cardsCount = resetBackupCardCount + 1)) + + dismissAndFinishFullReset() + } + + private fun checkRemainingBackupCards() { + val backupCardsCount = firstCardScanResponse.getBackupCardsCount() + + when { + backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog) + backupCardsCount == resetBackupCardCount -> { + analyticsEventHandler.send( + event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1), + ) + showDialog(CardSettingsDialog.CompletedResetDialog) + } + else -> finishFullReset() + } + } + + private fun dismissAndFinishFullReset() { + dismissDialog() + + finishFullReset() + } + + private fun finishFullReset() { + val newSelectedWallet = userWalletsListManager.selectedUserWalletSync + + if (newSelectedWallet != null) { + store.dispatch(NavigationAction.PopBackTo(AppScreen.Wallet)) + } else { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess + if (isLocked && userWalletsListManager.hasUserWallets) { + store.dispatch(NavigationAction.PopBackTo(AppScreen.Welcome)) + } else { + store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + } + } + } + + private fun showDialog(dialog: CardSettingsDialog) { + store.dispatch(ResetToFactory.ShowDialog(dialog)) + } + + private fun dismissDialog() { + store.dispatch(ResetToFactory.DismissDialog) + } + + private fun ScanResponse.getBackupCardsCount(): Int { + if (!cardTypesResolver.isMultiwalletAllowed()) return 0 + + return when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + is CardDTO.BackupStatus.CardLinked -> status.cardCount + is CardDTO.BackupStatus.NoBackup -> 0 + null -> 0 // Multi-currency wallet without backup function. Example, 4.12 + } + } + + private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId { + return UserWalletIdBuilder.scanResponse(scanResponse).build() + ?: error("UserWalletId can't be null") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt new file mode 100644 index 0000000000..2de1e70012 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.details.ui.resetcard.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.tap.features.details.ui.resetcard.featuretoggles.DefaultResetCardFeatureToggles +import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles +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 ResetCardModule { + + @Provides + @Singleton + fun provideResetCardFeatureToggles(featureTogglesManager: FeatureTogglesManager): ResetCardFeatureToggles { + return DefaultResetCardFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt new file mode 100644 index 0000000000..a5b76a410d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.details.ui.resetcard.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultResetCardFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : ResetCardFeatureToggles { + + override val isFullResetEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "FULL_RESET_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt new file mode 100644 index 0000000000..d290d4d1d9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.details.ui.resetcard.featuretoggles + +interface ResetCardFeatureToggles { + + val isFullResetEnabled: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index 2767f56545..c44b9ca3fa 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -17,7 +17,7 @@ import javax.inject.Inject internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel = SecurityModeViewModel(store) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 1733d8d946..19dd0771bc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -8,10 +8,9 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.WalletConnect -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +21,7 @@ import javax.inject.Inject internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: WalletConnectViewModel by viewModels() @@ -42,13 +41,6 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions + val sessions = state.wc2Sessions return WalletConnectScreenState( sessions.toImmutableList(), isLoading = state.loading, - onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions, state.wc2Sessions) }, + onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) }, onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, ) } - private fun onRemoveSession( - sessionUri: String, - sessions: List, - wc2sessions: List, - ) { - sessions - .firstOrNull { it.session.toUri() == sessionUri } - ?.let { baseSession -> - store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session.topic, baseSession.session)) - return - } + private fun onRemoveSession(sessionUri: String, wc2sessions: List) { wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let { - store.dispatch(WalletConnectAction.DisconnectSession(sessionUri, null)) + store.dispatch(WalletConnectAction.DisconnectSession(sessionUri)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt deleted file mode 100644 index d49a684033..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.store -import com.tangem.wallet.R - -object ApproveWcSessionDialog { - fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { - val sessionBlockchain = requireNotNull(session.wallet.blockchain) { "session network is null" } - val message = context.getString( - R.string.wallet_connect_request_session_start, - session.peerMeta.name, - sessionBlockchain.fullName, - session.peerMeta.url, - ) - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(context.getText(R.string.common_start)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.ChooseNetwork(sessionBlockchain)) - } - if (networks.size > 1) { - setNeutralButton(context.getText(R.string.wallet_connect_select_network)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.SelectNetwork(session = session, networks = networks)) - } - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) - } - setOnCancelListener { - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt index cddaab442a..aee88834e4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt @@ -42,13 +42,6 @@ object BnbTransactionDialog { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(fullMessage) setPositiveButton(positiveButtonTitle) { _, _ -> - store.dispatch( - WalletConnectAction.BinanceTransaction.Sign( - id = preparedData.requestId, - data = data.data, - topic = preparedData.topic, - ), - ) store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt deleted file mode 100644 index b99df2b02e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.store -import com.tangem.wallet.R - -object ChooseNetworkDialog { - fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog) - .setTitle(context.getString(R.string.wallet_connect_select_network)) - .setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> - store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) - } - .setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - .setSingleChoiceItems(networks.map { it.fullName }.toTypedArray(), 0) { _, which -> - networks.getOrNull(which)?.let { selectedBlockchain -> - store.dispatch( - WalletConnectAction.ChooseNetwork( - blockchain = selectedBlockchain, - ), - ) - store.dispatch(GlobalAction.HideDialog) - } - } - .create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt index 4230afdeaf..4403b6d4a6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt @@ -17,7 +17,6 @@ object PersonalSignDialog { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> - store.dispatch(WalletConnectAction.SignMessage(data.topic)) store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt index 608eebc130..3ec2d8bfda 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt @@ -32,11 +32,9 @@ object TransactionDialog { setMessage(message) setPositiveButton(positiveButtonTitle) { _, _ -> if (data.isEnoughFundsToSend) { - store.dispatch(WalletConnectAction.SendTransaction(data.topic)) store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } else { store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) - store.dispatch(WalletConnectAction.NotEnoughFunds) } } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 90bb65ce39..8e081474fb 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -12,9 +12,9 @@ import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.redux.AppState @@ -30,7 +30,7 @@ import javax.inject.Inject class HomeFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private var homeState: MutableState = mutableStateOf(store.state.homeState) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index bd3f2a5dde..1b0c69fb9c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -21,7 +21,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.resources.C +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags 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 @@ -57,7 +58,7 @@ fun StoriesScreen( } StoriesScreenContent( - modifier = Modifier.fillMaxSize().testTag(C.Tag.STORIES_SCREEN), + modifier = Modifier.fillMaxSize().testTag(TestTags.STORIES_SCREEN), config = StoriesScreenContentConfig( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, @@ -213,7 +214,7 @@ private data class StoriesScreenContentConfig( private fun StoriesScreenContentPreview( @PreviewParameter(StoriesScreenContentConfigProvider::class) config: StoriesScreenContentConfig, ) { - TangemTheme { + TangemThemePreview { StoriesScreenContent(config = config) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index 75d308e2b7..be6823d684 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -16,7 +16,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.resources.C +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags import com.tangem.wallet.R @Composable @@ -33,7 +34,7 @@ internal fun HomeButtons( ScanCardButton( modifier = Modifier .weight(weight = 1f) - .testTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON), + .testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON), showProgress = btnScanStateInProgress, onClick = onScanButtonClick, ) @@ -41,7 +42,7 @@ internal fun HomeButtons( OrderCardButton( modifier = Modifier .weight(weight = 1f) - .testTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON), + .testTag(TestTags.STORIES_SCREEN_ORDER_BUTTON), onClick = onShopButtonClick, ) } @@ -73,7 +74,7 @@ private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) @Preview(showBackground = true, widthDp = 360) @Composable private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::class) state: HomeButtonsState) { - TangemTheme { + TangemThemePreview { Box( modifier = Modifier.background(Color.Black), ) { diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt index 4e91ce98bc..e3e237954e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @Composable @@ -29,7 +30,7 @@ internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Mo @Preview(showBackground = true, widthDp = 360) @Composable private fun SearchCurrenciesButtonPreview() { - TangemTheme { + TangemThemePreview { Box( modifier = Modifier .background(color = Color.Black) 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 index 40110a5a22..92efb060a0 100644 --- 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 @@ -11,7 +11,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -19,11 +19,9 @@ import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL -import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -35,7 +33,7 @@ private const val HIDE_PROGRESS_DELAY = 400L object HomeMiddleware { val handler = homeMiddleware - const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/" + const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" } private val homeMiddleware: Middleware = { _, _ -> @@ -77,8 +75,10 @@ private fun handleHomeAction(action: Action) { } private suspend fun readCard() { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + isBiometricsRequestPolicy = shouldSaveAccessCodes, ) store.inject(DaggerGraphState::scanCardProcessor).scan( @@ -103,11 +103,13 @@ private suspend fun readCard() { } private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { - val userWallet = UserWalletBuilder(scanResponse).build().guard { + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).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") @@ -127,6 +129,8 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { ) if (currency != null) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + Analytics.send( event = Basic.SignedIn( currency = currency, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt index 7458f759ba..c420052753 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt @@ -7,5 +7,5 @@ import android.content.Intent */ interface IntentHandler { - fun handleIntent(intent: Intent?): Boolean + fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt index 696827a0e3..8ca7a84a1b 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentProcessor.kt @@ -19,9 +19,9 @@ class IntentProcessor { intentHandlers.clear() } - fun handleIntent(intent: Intent?) { + fun handleIntent(intent: Intent?, isFromForeground: Boolean) { intentHandlers.forEach { - it.handleIntent(intent) + it.handleIntent(intent, isFromForeground) } } } \ No newline at end of file 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 cd46a39fad..14c831cc90 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 @@ -25,7 +25,8 @@ class BackgroundScanIntentHandler( NfcAdapter.ACTION_TAG_DISCOVERED, ) - override fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { + if (isFromForeground) return true if (intent == null || intent.action !in nfcActions) return false val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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 23f8ec2235..273ca6d3b2 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 @@ -3,7 +3,6 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.removePrefixOrNull -import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.store @@ -15,12 +14,12 @@ import java.net.URLDecoder */ class WalletConnectLinkIntentHandler : IntentHandler { - override fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { val intentData = intent?.data ?: return false val scheme = intent.scheme ?: return false val wcUri = when (scheme) { - WalletConnectManager.WC_SCHEME -> intentData.toString() + WC_SCHEME -> intentData.toString() TANGEM_SCHEME -> intentData.toString().removePrefixOrNull(TANGEM_WC_PREFIX) else -> null } @@ -40,8 +39,9 @@ class WalletConnectLinkIntentHandler : IntentHandler { } private companion object { - private const val TANGEM_SCHEME = "tangem" - private const val TANGEM_WC_PREFIX = "tangem://wc?uri=" - private const val DEFAULT_CHARSET_NAME = "UTF-8" + const val TANGEM_SCHEME = "tangem" + const val TANGEM_WC_PREFIX = "tangem://wc?uri=" + const val DEFAULT_CHARSET_NAME = "UTF-8" + const val WC_SCHEME = "wc" } } \ No newline at end of file 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 d61c7ffebf..53a3565731 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 @@ -2,6 +2,9 @@ package com.tangem.tap.features.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController @@ -10,13 +13,20 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase +import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.main.model.MainScreenState +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -27,7 +37,12 @@ internal class MainViewModel @Inject constructor( private val reduxNavController: ReduxNavController, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, + private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, + private val blockchainSDKFactory: BlockchainSDKFactory, + private val userWalletsListManager: UserWalletsListManager, + private val walletManagersFacade: WalletManagersFacade, private val sendFeatureToggles: SendFeatureToggles, + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -41,7 +56,14 @@ internal class MainViewModel @Inject constructor( val state: StateFlow = stateHolder.stateFlow + var isSplashScreenShown: Boolean = true + private set + init { + loadApplicationResources() + + viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } + updateAppCurrencies() updateSendFeatureToggle() observeFlips() @@ -53,6 +75,40 @@ internal class MainViewModel @Inject constructor( } } + /** Loading the resources needed to run the application */ + private fun loadApplicationResources() { + viewModelScope.launch(dispatchers.main) { + blockchainSDKFactory.init() + prepareSelectedWalletFeedback() + + isSplashScreenShown = false + } + } + + private fun prepareSelectedWalletFeedback() { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { userWallet -> + Analytics.setContext(userWallet.scanResponse) + Analytics.send(Basic.WalletOpened()) + + if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) { + store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> + infoHolder.setCardInfo(userWallet.scanResponse) + + walletManagersFacade + .getAll(userWallet.walletId) + .distinctUntilChanged() + .onEach(infoHolder::setWalletsInfo) + .catch { Timber.e(it) } + .launchIn(viewModelScope) + } + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + private fun updateAppCurrencies() { viewModelScope.launch(dispatchers.main) { fetchAppCurrenciesUseCase.invoke() diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt index ec7cb8d416..ef72720cad 100644 --- a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.activityViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.ModalNotification import com.tangem.tap.features.main.ui.components.ModalNotificationContent @@ -22,7 +22,7 @@ import javax.inject.Inject internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: MainViewModel by activityViewModels() diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt index 587b734dc0..72c36caf54 100644 --- a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.main.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.main.model.ActionConfig import com.tangem.tap.features.main.model.ModalNotification @@ -82,27 +84,12 @@ internal fun ModalNotificationContent(notification: ModalNotification, modifier: // region Preview @Preview(widthDp = 360, heightDp = 404) +@Preview(widthDp = 360, heightDp = 404, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ModalNotificationContentPreview_Light( +private fun ModalNotificationContentPreview( @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, ) { - TangemTheme(isDark = false) { - ModalNotificationContent( - notification = param, - modifier = Modifier.background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 404) -@Composable -private fun ModalNotificationContentPreview_Dark( - @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ModalNotificationContent( notification = param, modifier = Modifier.background( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 050f08d79e..f03dc73425 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -12,14 +12,17 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.userwallets.UserWalletIdBuilder -import com.tangem.tap.* +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.saveWallet.redux.SaveWalletAction +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber @@ -79,6 +82,8 @@ object OnboardingHelper { ) { Analytics.setContext(scanResponse) scope.launch { + val settingsRepository = store.inject(DaggerGraphState::settingsRepository) + when { // When should save user wallets, then save card without navigate to save wallet screen store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { @@ -90,16 +95,11 @@ object OnboardingHelper { ), ) - val toggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - if (toggles.isGeneralManagerEnabled) { - store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError)) - } else { - store.dispatchWithMain(SaveWalletAction.Save) - } + store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError)) } // When should not save user wallets but device has biometry and save wallet screen has not been shown, // then open save wallet screen - tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> { + tangemSdkManager.canUseBiometry && settingsRepository.shouldShowSaveUserWalletScreen() -> { proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) delay(timeMillis = 1_200) @@ -142,7 +142,8 @@ object OnboardingHelper { backupCardsIds: List?, hasBackupError: Boolean, ) { - val userWallet = UserWalletBuilder(scanResponse = scanResponse) + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() @@ -151,6 +152,7 @@ object OnboardingHelper { return } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index ca78c542d3..ef5296a6b4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -23,12 +23,14 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.mainScope +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware @@ -120,8 +122,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch val walletManager = if (noteState.walletManager != null) { noteState.walletManager } else { - val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard { + val wmFactory = runBlocking { + store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() + } + val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard { val message = "Loading cancelled. Cause: wallet manager didn't created" val customError = TapError.CustomError(message) store.dispatchErrorNotification(customError) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index 4aa4e8a4d4..ca00a0c255 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.Message import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.VoidCallback -import com.tangem.datasource.asset.AssetReader import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager @@ -27,7 +26,7 @@ sealed class TwinCardsAction : Action { } sealed class Wallet : TwinCardsAction() { - data class LaunchFirstStep(val initialMessage: Message, val reader: AssetReader) : TwinCardsAction() + data class LaunchFirstStep(val initialMessage: Message) : TwinCardsAction() data class LaunchSecondStep( val initialMessage: Message, val preparingMessage: Message, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index e973dae678..033b790e84 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -12,7 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.asLockable import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -32,10 +32,10 @@ import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware @@ -60,6 +60,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager val twinCardsState = store.state.twinCardsState + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) fun getScanResponse(): ScanResponse { return when (twinCardsState.mode) { @@ -164,10 +165,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } } is TwinCardsAction.Wallet.LaunchFirstStep -> { - val manager = TwinCardsManager( - card = getScanResponse().card, - assetReader = action.reader, - ) + val manager = TwinCardsManager(card = getScanResponse().card) store.dispatch(TwinCardsAction.CardsManager.Set(manager)) scope.launch { @@ -244,8 +242,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val walletManager = if (twinCardsState.walletManager != null) { twinCardsState.walletManager } else { - val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletManager = wmFactory.makePrimaryWalletManager(getScanResponse()).guard { + val wmFactory = runBlocking { + store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() + } + val walletManager = wmFactory?.makePrimaryWalletManager(getScanResponse()).guard { val message = "Loading cancelled. Cause: wallet manager didn't created" val customError = TapError.CustomError(message) store.dispatchErrorNotification(customError) @@ -363,6 +363,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } private fun getPopBackScreen(): AppScreen { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + return if (userWalletsListManager.hasUserWallets) { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index 75e54a693c..2cbf5949d1 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -15,10 +15,9 @@ import com.tangem.common.extensions.VoidCallback import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.ShareElement import com.tangem.core.ui.extensions.setStatusBarColor -import com.tangem.datasource.asset.AssetReader import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.Artwork +import com.tangem.domain.wallets.models.Artwork import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.* @@ -35,15 +34,11 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject @Suppress("LargeClass") @AndroidEntryPoint internal class OnboardingTwinsFragment : BaseOnboardingFragment() { - @Inject - lateinit var assetReader: AssetReader - private val mainBinding by lazy { binding.vMain } private var previousStep: TwinCardsStep = TwinCardsStep.None @@ -88,13 +83,13 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( binding.toolbar.title = getText(R.string.twins_recreate_toolbar) - mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) { + mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1_URL) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) fallback(R.drawable.card_placeholder_black) } - mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) { + mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2_URL) { placeholder(R.drawable.card_placeholder_white) error(R.drawable.card_placeholder_white) fallback(R.drawable.card_placeholder_white) @@ -260,7 +255,6 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( store.dispatch( TwinCardsAction.Wallet.LaunchFirstStep( initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - reader = assetReader, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 5c29c96fb9..925dc9bcc2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -11,13 +11,12 @@ import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.Artwork -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.models.Artwork +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator @@ -418,15 +417,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) when (val error = result.error) { is TangemSdkError.BackupFailedNotEmptyWallets -> { - if (card?.canSkipBackup == false) { - store.dispatchOnMain( - GlobalAction.ShowDialog( - BackupDialog.ResetBackupCard(error.cardId), - ), - ) - } else { - crashlytics.recordException(error) - } + store.dispatchOnMain( + GlobalAction.ShowDialog( + BackupDialog.ResetBackupCard(error.cardId), + ), + ) } is TangemSdkError.IssuerSignatureLoadingFailed -> { store.dispatchOnMain( @@ -497,7 +492,17 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)) } } - is CompletionResult.Failure -> Unit + is CompletionResult.Failure -> { + when (val error = result.error) { + is TangemSdkError.BackupFailedNotEmptyWallets -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + BackupDialog.ResetBackupCard(error.cardId), + ), + ) + } + } + } } } } @@ -543,7 +548,8 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } if (scanResponse != null) { - val userWallet = UserWalletBuilder(scanResponse) + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) .backupCardsIds(backupState.backupCardIds.toSet()) .build() .guard { @@ -551,6 +557,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) return@launch } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.update( userWalletId = userWallet.walletId, update = { wallet -> @@ -562,7 +569,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) ) }, ) - store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager)) } val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt index ff18f45362..e79ea8b9fe 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt @@ -11,13 +11,11 @@ internal sealed interface SaveWalletAction : Action { val backupCardsIds: Set?, ) : SaveWalletAction - data object Save : SaveWalletAction { + data object AllowToUseBiometrics : SaveWalletAction { data object Success : SaveWalletAction data class Error(val error: TangemError) : SaveWalletAction } - data object AllowToUseBiometrics : SaveWalletAction - data object Dismiss : SaveWalletAction data object CloseError : SaveWalletAction @@ -26,7 +24,5 @@ internal sealed interface SaveWalletAction : Action { data object Cancel : SaveWalletAction } - data object SaveWalletWasShown : SaveWalletAction - data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 5f110e31a9..45eb5225a2 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -6,10 +6,8 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding @@ -18,9 +16,11 @@ import com.tangem.tap.common.extensions.dispatchWithMain 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.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch @@ -46,16 +46,14 @@ internal class SaveWalletMiddleware { private fun handleAction(action: SaveWalletAction, state: SaveWalletState) { when (action) { - is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state) is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state) is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics() - is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown() is SaveWalletAction.Dismiss -> dismiss(state) is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state, action.hasBackupError) - is SaveWalletAction.Save.Success, + is SaveWalletAction.AllowToUseBiometrics.Success, + is SaveWalletAction.AllowToUseBiometrics.Error, is SaveWalletAction.ProvideBackupInfo, is SaveWalletAction.CloseError, - is SaveWalletAction.Save.Error, is SaveWalletAction.EnrollBiometrics, is SaveWalletAction.EnrollBiometrics.Cancel, -> Unit @@ -66,7 +64,8 @@ internal class SaveWalletMiddleware { scope.launch { val backupInfo = state.backupInfo ?: error("Backup info is null") - val userWallet = UserWalletBuilder(backupInfo.scanResponse) + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(backupInfo.scanResponse, walletNameGenerateUseCase) .backupCardsIds(state.backupInfo.backupCardsIds) .hasBackupError(hasBackupError) .build() @@ -75,6 +74,7 @@ internal class SaveWalletMiddleware { return@launch } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .flatMap { saveAccessCodeIfNeeded(accessCode = backupInfo.accessCode, cardsInWallet = userWallet.cardsInWallet) @@ -91,82 +91,6 @@ internal class SaveWalletMiddleware { store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) } - private fun saveWalletIfBiometricsEnrolled(state: SaveWalletState) { - if (tangemSdkManager.needEnrollBiometrics) { - store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) - } else { - saveWallet(state) - } - } - - /** - - * or from [SaveWalletState.backupInfo] if provided from - * [com.tangem.tap.features.onboarding.OnboardingHelper.trySaveWalletAndNavigateToWalletScreen] - * - * If saved user's wallet was selected then pop back to [AppScreen.Wallet] - * or navigate to [AppScreen.WalletSelector] otherwise - * - * TODO: Update that logic after onboarding and backup features refactoring - * */ - private fun saveWallet(state: SaveWalletState) { - val scanResponse = state.backupInfo?.scanResponse - ?: store.state.globalState.scanResponse - ?: return - - if (state.backupInfo != null) { - // TODO: Remove after onboarding refactoring - Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On)) - } else { - Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) - } - - scope.launch { - val userWallet = userWalletsListManager.selectedUserWalletSync - ?: UserWalletBuilder(scanResponse) - .backupCardsIds(state.backupInfo?.backupCardsIds) - .build() - ?: return@launch - - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - if (!featureToggles.isGeneralManagerEnabled) { - provideLockableUserWalletsListManagerIfNot() - } - - val isFirstSavedWallet = !userWalletsListManager.hasUserWallets - - saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet) - .flatMap { - // Save wallet only at first time (SaveWalletBottomSheet). - // Otherwise (Example, add new wallet in Details) userWalletsListManager.wallets subscribers will - // receive useless updates. - // See: OnboardingHelper.trySaveWalletAndNavigateToWalletScreen() - if (isFirstSavedWallet) { - userWalletsListManager.save(userWallet, canOverride = true) - } else { - CompletionResult.Success(Unit) - } - } - .doOnFailure { error -> - store.dispatchWithMain(SaveWalletAction.Save.Error(error)) - } - .doOnSuccess { - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - - // Enable saving access codes only if this is the first time user save the wallet - if (isFirstSavedWallet) { - preferencesStorage.shouldSaveAccessCodes = true - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, - ) - } - - store.dispatchOnMain(SaveWalletAction.Save.Success) - store.navigateToWallet() - } - }.saveIn(saveWalletJobHolder) - } - private fun allowToUseBiometrics(state: SaveWalletState) { if (tangemSdkManager.needEnrollBiometrics) { store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) @@ -185,10 +109,13 @@ internal class SaveWalletMiddleware { * because it will be automatically saved on UserWalletsListManager switch */ + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { val error = IllegalStateException("No selected user wallet") Timber.e(error, "Unable to save user wallet") - store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) + store.dispatchWithMain( + SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)), + ) return@launch } @@ -198,29 +125,17 @@ internal class SaveWalletMiddleware { private suspend fun handleSuccessAllowing(userWallet: UserWallet) { store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - preferencesStorage.shouldSaveAccessCodes = true + + store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true) + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = userWallet.hasAccessCode, ) - store.dispatchWithMain(SaveWalletAction.Save.Success) + store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) } - private suspend fun provideLockableUserWalletsListManagerIfNot() { - if (store.state.globalState.userWalletsListManager?.isLockable() == true) return - - val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard { - val error = IllegalStateException("No activities in foreground") - Timber.e(error) - store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) - return - } - val manager = UserWalletsListManager.provideBiometricImplementation(context) - - store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) - } - private fun dismiss(state: SaveWalletState) { if (state.backupInfo != null) { // TODO: Remove after onboarding refactoring @@ -230,10 +145,6 @@ internal class SaveWalletMiddleware { } } - private fun saveWalletWasShown() { - preferencesStorage.shouldShowSaveUserWalletScreen = false - } - private suspend fun saveAccessCodeIfNeeded( accessCode: String?, cardsInWallet: Set, diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt index 88b5697eca..370f7cd935 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt @@ -21,14 +21,13 @@ internal object SaveWalletReducer { backupCardsIds = action.backupCardsIds, ), ) - is SaveWalletAction.Save, is SaveWalletAction.AllowToUseBiometrics, -> state.copy(isSaveInProgress = true) - is SaveWalletAction.Save.Error -> state.copy( + is SaveWalletAction.AllowToUseBiometrics.Error -> state.copy( error = action.error, isSaveInProgress = false, ) - is SaveWalletAction.Save.Success -> state.copy( + is SaveWalletAction.AllowToUseBiometrics.Success -> state.copy( backupInfo = null, isSaveInProgress = false, ) @@ -47,7 +46,6 @@ internal object SaveWalletReducer { needEnrollBiometrics = false, isSaveInProgress = false, ) - is SaveWalletAction.SaveWalletWasShown, is SaveWalletAction.SaveWalletAfterBackup, -> state } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt index fe5792ad29..d0d7c64fb5 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -11,9 +11,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent @@ -25,7 +25,7 @@ import javax.inject.Inject internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies override val expandedHeightFraction: Float = .98f diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index 99947493fc..40164976d3 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -1,9 +1,10 @@ package com.tangem.tap.features.saveWallet.ui import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles +import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.ui.cardsettings.TextReference @@ -11,34 +12,36 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog import com.tangem.tap.store +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber import javax.inject.Inject @HiltViewModel internal class SaveWalletViewModel @Inject constructor( - private val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, + private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, + dispatchers: AppCoroutineDispatcherProvider, ) : ViewModel(), StoreSubscriber { + private val stateInternal = MutableStateFlow(SaveWalletScreenState()) val state: StateFlow = stateInternal init { subscribeToStoreChanges() - store.dispatchOnMain(SaveWalletAction.SaveWalletWasShown) + + viewModelScope.launch(dispatchers.main) { + setSaveWalletScreenShownUseCase() + } } fun saveWallet() { analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) - - if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { - store.dispatch(SaveWalletAction.AllowToUseBiometrics) - } else { - store.dispatch(SaveWalletAction.Save) - } + store.dispatch(SaveWalletAction.AllowToUseBiometrics) } fun cancelOrClose() { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index 9113d5001d..115bd8d273 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.saveWallet.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable @@ -9,6 +10,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @@ -40,17 +42,10 @@ private fun EnrollBiometricDialogContentSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun EnrollBiometricDialogContentPreview_Light() { - TangemTheme { - EnrollBiometricDialogContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun EnrollBiometricDialogContentPreview_Dark() { - TangemTheme(isDark = true) { +private fun EnrollBiometricDialogContentPreview() { + TangemThemePreview { EnrollBiometricDialogContentSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index a8bbc46f02..c7679a466d 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.saveWallet.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.SpacerHHalf import com.tangem.core.ui.components.SpacerW24 import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -196,17 +198,10 @@ private fun SaveWalletScreenContentSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SaveWalletScreenContentPreview_Light() { - TangemTheme { - SaveWalletScreenContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SaveWalletScreenContentPreview_Dark() { - TangemTheme(isDark = true) { +private fun SaveWalletScreenContentPreview() { + TangemThemePreview { SaveWalletScreenContentSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index cfdcf74a19..e3096c6a4f 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -1,19 +1,21 @@ package com.tangem.tap.features.send.redux.middlewares -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero -import com.tangem.tap.common.redux.AppState import com.tangem.domain.demo.DemoTransactionSender +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.AmountActionUi import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.ReceiptAction import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -27,6 +29,7 @@ import java.math.BigDecimal */ class RequestFeeMiddleware { + @Suppress("CyclomaticComplexMethod") fun handle(appState: AppState?, dispatch: DispatchFunction) { val sendState = appState?.sendState ?: return val walletManager = sendState.walletManager ?: return @@ -45,7 +48,7 @@ class RequestFeeMiddleware { val txSender = if (scanResponse.isDemoCard()) { DemoTransactionSender(walletManager) } else { - walletManager as TransactionSender + walletManager } scope.launch { val feeResult = txSender.getFee(destinationAmount, destinationAddress) @@ -73,12 +76,17 @@ class RequestFeeMiddleware { dispatch(FeeAction.FeeCalculation.ClearResult) dispatch(FeeAction.ChangeLayoutVisibility(main = false)) - store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - walletManager = walletManager, - amountToSend = destinationAmount, - feeAmount = null, - destinationAddress = destinationAddress, - ) + val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) + if (featureToggles.isLocalLogsEnabled) { + saveBlockchainError(feeResult, destinationAddress, destinationAmount, walletManager) + } else { + store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( + walletManager = walletManager, + amountToSend = destinationAmount, + feeAmount = null, + destinationAddress = destinationAddress, + ) + } val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext dispatch( @@ -93,4 +101,28 @@ class RequestFeeMiddleware { } } } + + private fun saveBlockchainError( + feeResult: Result.Failure, + destinationAddress: String, + destinationAmount: Amount, + walletManager: WalletManager, + ) { + store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( + error = BlockchainErrorInfo( + errorMessage = (feeResult.error as? BlockchainSdkError)?.customMessage + ?: "It isn't BlockchainSdkError", + blockchainId = walletManager.wallet.blockchain.id, + derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", + destinationAddress = destinationAddress, + tokenSymbol = if (destinationAmount.type is AmountType.Token) { + destinationAmount.currencySymbol + } else { + "" + }, + amount = destinationAmount.value?.stripZeroPlainString() ?: "0", + fee = null, + ), + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index e85db98c7b..756b65e48d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -4,22 +4,24 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin -import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.demo.DemoTransactionSender +import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token @@ -31,7 +33,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.domain.demo.DemoTransactionSender import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.FeeAction.RequestFee @@ -170,11 +171,7 @@ private fun sendTransaction( transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) } transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) } transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) } - transactionExtras.hederaMemoState?.memo?.let { - txData = txData.copy( - extras = HederaTransactionBuilder.HederaTransactionExtras(it), - ) - } + transactionExtras.hederaMemoState?.memo?.let { txData = txData.copy(extras = HederaTransactionExtras(it)) } transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) } scope.launch { @@ -249,7 +246,7 @@ private fun sendTransaction( tangemSdk.config.linkedTerminal = linkedTerminalState when (sendResult) { - is SimpleResult.Success -> { + is Result.Success -> { dispatch(SendAction.SendSuccess) if (externalTransactionData != null) { @@ -276,8 +273,9 @@ private fun sendTransaction( dispatch(NavigationAction.PopBackTo()) } } - is SimpleResult.Failure -> { + is Result.Failure -> { updateFeedbackManagerInfo( + sendResult = sendResult.error, walletManager = walletManager, amountToSend = amountToSend, feeAmount = fee.amount, @@ -368,13 +366,33 @@ private fun updateFeedbackManagerInfo( amountToSend: Amount, feeAmount: Amount, destinationAddress: String, + sendResult: BlockchainError, ) { - store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - walletManager = walletManager, - amountToSend = amountToSend, - feeAmount = feeAmount, - destinationAddress = destinationAddress, - ) + val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) + if (featureToggles.isLocalLogsEnabled) { + store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( + error = BlockchainErrorInfo( + errorMessage = (sendResult as? BlockchainSdkError)?.customMessage ?: "It isn't BlockchainSdkError", + blockchainId = walletManager.wallet.blockchain.id, + derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", + destinationAddress = destinationAddress, + tokenSymbol = if (amountToSend.type is AmountType.Token) { + amountToSend.currencySymbol + } else { + "" + }, + amount = amountToSend.value?.stripZeroPlainString() ?: "0", + fee = feeAmount.value?.stripZeroPlainString() ?: "0", + ), + ) + } else { + store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( + walletManager = walletManager, + amountToSend = amountToSend, + feeAmount = feeAmount, + destinationAddress = destinationAddress, + ) + } } fun createValidateTransactionError( diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt index a50de375f4..dbf1db4f40 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt @@ -330,6 +330,7 @@ class ReceiptReducer : SendInternalReducer { FeePaidCurrency.Coin -> wallet.blockchain.currency FeePaidCurrency.SameCurrency -> store.state.sendState.currency?.symbol is FeePaidCurrency.Token -> feePaidCurrency.token.symbol + is FeePaidCurrency.FeeResource -> feePaidCurrency.currency }, ) } @@ -356,9 +357,12 @@ class ReceiptReducer : SendInternalReducer { FeePaidCurrency.Coin -> when (amountType) { AmountType.Coin -> ReceiptLayoutType.FIAT is AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT - AmountType.Reserve -> ReceiptLayoutType.UNKNOWN + is AmountType.FeeResource, + AmountType.Reserve, + -> ReceiptLayoutType.UNKNOWN } FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY_FIAT + is FeePaidCurrency.FeeResource -> ReceiptLayoutType.SAME_CURRENCY } } @@ -374,8 +378,11 @@ class ReceiptReducer : SendInternalReducer { FeePaidCurrency.Coin -> when (amountType) { AmountType.Coin -> ReceiptLayoutType.CRYPTO is AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO - AmountType.Reserve -> ReceiptLayoutType.UNKNOWN + is AmountType.FeeResource, + AmountType.Reserve, + -> ReceiptLayoutType.UNKNOWN } + is FeePaidCurrency.FeeResource -> ReceiptLayoutType.UNKNOWN } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index f71667eed8..9ac9254fec 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -122,6 +122,7 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer { sendToken.name.equals(feePaidCurrency.token.name, ignoreCase = true) && sendToken.symbol.equals(feePaidCurrency.token.symbol, ignoreCase = true) } + is FeePaidCurrency.FeeResource -> false } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index b953ab71dd..d3d3cd7e19 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -82,14 +82,18 @@ data class SendState( fun convertFiatToExtractCrypto(fiatValue: BigDecimal): BigDecimal = when (amountState.typeOfAmount) { AmountType.Coin -> convertFiatToCoin(fiatValue) is AmountType.Token -> convertFiatToToken(fiatValue) - AmountType.Reserve -> fiatValue + is AmountType.FeeResource, + AmountType.Reserve, + -> fiatValue } fun convertExtractCryptoToFiat(cryptoValue: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal { return when (amountState.typeOfAmount) { AmountType.Coin -> convertCoinToFiat(cryptoValue, scaleWithPrecision) is AmountType.Token -> convertTokenToFiat(cryptoValue, scaleWithPrecision) - AmountType.Reserve -> cryptoValue + is AmountType.FeeResource, + AmountType.Reserve, + -> cryptoValue } } @@ -108,7 +112,9 @@ data class SendState( return when (amountState.typeOfAmount) { AmountType.Coin -> coinIsConvertible() is AmountType.Token -> tokenIsConvertible() - AmountType.Reserve -> false + is AmountType.FeeResource, + AmountType.Reserve, + -> false } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index f036004238..dcacf9fef2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -3,11 +3,11 @@ package com.tangem.tap.features.tokens.impl.data import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.isSupportedInApp +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt index 2bffd45d01..2d620d45e8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.tokens.impl.data.converters import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.utils.converter.Converter diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt index d1babd19b7..561c8c123a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.tokens.impl.data.converters import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.local.testnet.models.TestnetTokensConfig -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.utils.converter.Converter diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index a6f68a2097..272c773c95 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel import dagger.hilt.android.AndroidEntryPoint @@ -23,7 +23,7 @@ import javax.inject.Inject internal class TokensListFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt index 806c161a01..12620898b6 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -22,6 +23,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import kotlinx.collections.immutable.ImmutableCollection @@ -135,9 +137,10 @@ private fun RowScope.NetworkTitle(model: NetworkItemState) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_DetailedNetworksList_ManageAccess_Light() { - TangemTheme { +private fun Preview_DetailedNetworksList_ManageAccess() { + TangemThemePreview { DetailedNetworksList( isExpanded = true, token = TokenListPreviewData.createManageToken(), @@ -147,33 +150,10 @@ private fun Preview_DetailedNetworksList_ManageAccess_Light() { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_DetailedNetworksList_ManageAccess_Dark() { - TangemTheme { - DetailedNetworksList( - isExpanded = true, - token = TokenListPreviewData.createManageToken(), - networks = TokenListPreviewData.createManageNetworksList(), - ) - } -} - -@Preview -@Composable -private fun Preview_DetailedNetworksList_ReadAccess_Light() { - TangemTheme { - DetailedNetworksList( - isExpanded = true, - token = TokenListPreviewData.createReadToken(), - networks = TokenListPreviewData.createReadNetworksList(), - ) - } -} - -@Preview -@Composable -private fun Preview_DetailedNetworksList_ReadAccess_Dark() { - TangemTheme { +private fun Preview_DetailedNetworksList_ReadAccess() { + TangemThemePreview { DetailedNetworksList( isExpanded = true, token = TokenListPreviewData.createReadToken(), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt index 671961ab61..b51b65f1c0 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -9,6 +10,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -39,25 +41,10 @@ internal fun NetworkItemArrow(itemHeight: Dp, isLastItem: Boolean) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NetworkItemArrow_Column_Light() { - TangemTheme(isDark = false) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.primary) - .padding(start = TangemTheme.dimens.size36), - ) { - NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = false) - NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = true) - } - } -} - -@Preview -@Composable -private fun Preview_NetworkItemArrow_Column_Dark() { - TangemTheme(isDark = true) { +private fun Preview_NetworkItemArrow_Column() { + TangemThemePreview { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt index bae044a2a6..ce1967b48b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme @@ -24,6 +25,7 @@ import androidx.constraintlayout.compose.Dimension import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.CurrencyPlaceholderIcon +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState @@ -216,33 +218,19 @@ private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, m } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenItem_ManageAccess_Light() { - TangemTheme(isDark = false) { - TokenItem(model = TokenListPreviewData.createManageToken()) - } -} - -@Preview() -@Composable -private fun Preview_TokenItem_ManageAccess_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TokenItem_ManageAccess() { + TangemThemePreview { TokenItem(model = TokenListPreviewData.createManageToken()) } } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenItem_ReadAccess_Light() { - TangemTheme(isDark = false) { - TokenItem(model = TokenListPreviewData.createReadToken()) - } -} - -@Preview() -@Composable -private fun Preview_TokenItem_ReadAccess_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TokenItem_ReadAccess() { + TangemThemePreview { TokenItem(model = TokenListPreviewData.createReadToken()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 70335b9609..56366dfb51 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade import androidx.compose.foundation.background @@ -34,6 +35,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder @@ -193,21 +195,12 @@ private fun SaveChangesButton(showProgress: Boolean, onClick: () -> Unit, modifi } @Preview(showSystemUi = true) +@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokensListScreen_Light( +private fun Preview_TokensListScreen( @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, ) { - TangemTheme(isDark = false) { - TokensListScreen(stateHolder) - } -} - -@Preview(showSystemUi = true) -@Composable -private fun Preview_TokensListScreen_Dark( - @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokensListScreen(stateHolder) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt index 71aba31a33..5e0aafc999 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.InputField import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.Title @@ -171,7 +172,7 @@ private fun Hint(value: String) { @Preview @Composable private fun Preview_AddTokensToolbar_EditAccess() { - TangemTheme { + TangemThemePreview { TokensListToolbar( state = Title.Manage( titleResId = R.string.main_manage_tokens, @@ -186,7 +187,7 @@ private fun Preview_AddTokensToolbar_EditAccess() { @Preview @Composable private fun Preview_AddTokensToolbar_ReadAccess() { - TangemTheme { + TangemThemePreview { TokensListToolbar( state = Title.Read( titleResId = R.string.common_search_tokens, @@ -202,7 +203,7 @@ private fun Preview_AddTokensToolbar_ReadAccess() { private fun Preview_AddTokensToolbar_SearchInputField() { var value by remember { mutableStateOf("") } - TangemTheme { + TangemThemePreview { TokensListToolbar( state = InputField( onBackButtonClick = {}, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 0fa1976240..53867da367 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.* import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction @@ -19,7 +20,6 @@ import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase 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 dd239f1135..abfc205397 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 @@ -13,16 +13,19 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable -import com.tangem.tap.* +import com.tangem.tap.backupService 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 com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -67,7 +70,7 @@ internal class WelcomeMiddleware { scope = scope, hasSavedUserWalletsProvider = { true }, ) - val isBackgroundScanHandled = handler.handleIntent(initialIntent) + val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false) val hasUncompletedBackup = backupService.hasIncompletedBackup if (!isBackgroundScanHandled && !hasUncompletedBackup) { @@ -83,6 +86,7 @@ internal class WelcomeMiddleware { """.trimIndent(), ) + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.unlockIfLockable(type = UnlockType.ANY) .doOnFailure { error -> Timber.e(error, "Unable to unlock user wallets with biometrics") @@ -99,7 +103,7 @@ internal class WelcomeMiddleware { store.onUserWalletSelected(userWallet = selectedUserWallet) afterUnlockIntent?.let { - WalletConnectLinkIntentHandler().handleIntent(it) + WalletConnectLinkIntentHandler().handleIntent(it, false) } } } @@ -113,8 +117,11 @@ internal class WelcomeMiddleware { ) scanCardInternal { scanResponse -> - val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build() + ?: return@scanCardInternal + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -128,7 +135,7 @@ internal class WelcomeMiddleware { store.onUserWalletSelected(userWallet = userWallet) afterScanIntent?.let { - WalletConnectLinkIntentHandler().handleIntent(it) + WalletConnectLinkIntentHandler().handleIntent(it, false) } } } @@ -140,12 +147,14 @@ internal class WelcomeMiddleware { ) Analytics.addContext(scanResponse) if (currency != null) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + Analytics.send( event = Basic.SignedIn( currency = currency, batch = scanResponse.card.batchId, signInType = signInType, - walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(), + walletsCount = userWalletsListManager.walletsCount.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) @@ -153,6 +162,7 @@ internal class WelcomeMiddleware { } private suspend fun disableUserWalletsSaving() { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.clear() .flatMap { tangemSdkManager.clearSavedUserCodes() } .doOnFailure { e -> @@ -165,9 +175,12 @@ internal class WelcomeMiddleware { } private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + isBiometricsRequestPolicy = shouldSaveAccessCodes, ) + store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsSource = AnalyticsParam.ScreensSources.SignIn, onSuccess = { scanResponse -> diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index c3197818c0..60ab61900f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -15,10 +15,10 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -31,7 +31,7 @@ import javax.inject.Inject internal class WelcomeFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies override fun onStart() { super.onStart() diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 1f4b9e650e..bc672b7c2f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -31,14 +31,12 @@ internal class WelcomeViewModel @Inject constructor( private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal - init { + override fun onCreate(owner: LifecycleOwner) { store.dispatch(WelcomeAction.SetCoroutineScope(viewModelScope)) subscribeToStoreChanges() initGlobalState() - } - override fun onCreate(owner: LifecycleOwner) { val welcomeAction = if (initialIntent != null) { WelcomeAction.ProceedWithIntent(initialIntent) } else { @@ -81,9 +79,10 @@ internal class WelcomeViewModel @Inject constructor( } } - override fun onCleared() { + override fun onDestroy(owner: LifecycleOwner) { store.dispatch(WelcomeAction.ClearCoroutineScope) store.unsubscribe(this) + super.onDestroy(owner) } private fun createWarningIfNeeded(error: TangemError?): WarningModel? { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt index ca9a8d5769..a6315d5b1e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.ui.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -7,7 +8,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.wallet.R @@ -72,17 +73,10 @@ private fun BiometricsLockoutDialogSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BiometricsLockoutDialogPreview_Light() { - TangemTheme { - BiometricsLockoutDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialogPreview_Dark() { - TangemTheme(isDark = true) { +private fun BiometricsLockoutDialogPreview() { + TangemThemePreview { BiometricsLockoutDialogSample() } } @@ -100,17 +94,10 @@ private fun BiometricsLockoutDialog_Permanent_Sample(modifier: Modifier = Modifi } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BiometricsLockoutDialog_Permanent_Preview_Light() { - TangemTheme { - BiometricsLockoutDialog_Permanent_Sample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialog_Permanent_Preview_Dark() { - TangemTheme(isDark = true) { +private fun BiometricsLockoutDialog_Permanent_Preview() { + TangemThemePreview { BiometricsLockoutDialog_Permanent_Sample() } } @@ -123,17 +110,10 @@ private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun KeyInvalidatedWarningPreview_Light() { - TangemTheme { - KeyInvalidatedWarningSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun KeyInvalidatedWarningPreview_Dark() { - TangemTheme(isDark = true) { +private fun KeyInvalidatedWarningPreview() { + TangemThemePreview { KeyInvalidatedWarningSample() } } @@ -146,17 +126,10 @@ private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BiometricDisabledWarningPreview_Light() { - TangemTheme { - BiometricDisabledWarningSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricDisabledWarningPreview_Dark() { - TangemTheme(isDark = true) { +private fun BiometricDisabledWarningPreview() { + TangemThemePreview { BiometricDisabledWarningSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt index cc83e0d893..5aca214e93 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -12,6 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -97,17 +99,10 @@ private fun WelcomeScreenContentSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun WelcomeScreenContentPreview_Light() { - TangemTheme { - WelcomeScreenContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun WelcomeScreenContentPreview_Dark() { - TangemTheme(isDark = true) { +private fun WelcomeScreenContentPreview() { + TangemThemePreview { WelcomeScreenContentSample() } } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAppVersionProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAppVersionProvider.kt index 7987d7ce6e..a5093725e5 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAppVersionProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAppVersionProvider.kt @@ -1,11 +1,11 @@ package com.tangem.tap.network.auth -import com.tangem.lib.auth.AppVersionProvider +import com.tangem.utils.version.AppVersionProvider import com.tangem.wallet.BuildConfig internal class DefaultAppVersionProvider : AppVersionProvider { - override fun getAppVersion(): String { - return BuildConfig.VERSION_NAME - } + override val versionName: String = BuildConfig.VERSION_NAME + + override val versionCode: Int = BuildConfig.VERSION_CODE } \ No newline at end of file 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 27ac9a4336..16427253e8 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 @@ -1,7 +1,7 @@ package com.tangem.tap.network.auth import com.tangem.common.extensions.toHexString -import com.tangem.lib.auth.AuthProvider +import com.tangem.datasource.api.common.AuthProvider import com.tangem.tap.proxy.AppStateHolder internal class DefaultAuthProvider(private val appStateHolder: AppStateHolder) : AuthProvider { diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt index 6cd26d788e..f7b18ad5a2 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt @@ -15,11 +15,11 @@ internal class DefaultExpressAuthProvider( private var uuid = AtomicReference(UUID.randomUUID()) override fun getApiKey(): String { - return configManager.config.express?.apiKey ?: "" + return configManager.config.express?.apiKey ?: error("No express api key provided") } override fun getUserId(): String { - return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: "" + return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: error("No user id provided") } override fun getSessionId(): String { diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt new file mode 100644 index 0000000000..7e953fdfd5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.network.auth + +import com.tangem.datasource.config.ConfigManager +import com.tangem.lib.auth.StakeKitAuthProvider + +internal class DefaultStakeKitAuthProvider( + private val configManager: ConfigManager, +) : StakeKitAuthProvider { + + override fun getApiKey(): String { + return configManager.config.express?.apiKey ?: error("No StakeKit api key provided") + } +} \ 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 0185472a6e..102598b998 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 @@ -1,14 +1,16 @@ package com.tangem.tap.network.auth.di +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.lib.auth.AppVersionProvider -import com.tangem.lib.auth.AuthProvider import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAuthProvider import com.tangem.tap.network.auth.DefaultExpressAuthProvider +import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -37,6 +39,12 @@ class AuthModule { ) } + @Provides + @Singleton + fun provideStakeKitAuthProvider(configManager: ConfigManager): StakeKitAuthProvider { + return DefaultStakeKitAuthProvider(configManager) + } + @Provides @Singleton fun provideAppVersionProvider(): AppVersionProvider { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index 7ed890dda8..041b9561ee 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -5,7 +5,9 @@ import com.tangem.blockchain.common.Token import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.converter.TwoWayConverter @@ -20,9 +22,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter - get() = _userWalletsListManagerFlow - - private val _userWalletsListManagerFlow = MutableStateFlow(null) +class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolder { @Deprecated("Use scan response from selected user wallet") var scanResponse: ScanResponse? = null @@ -69,4 +56,8 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl override suspend fun onUserWalletSelected(userWallet: UserWallet) { mainStore?.onUserWalletSelected(userWallet) } + + override fun sendFeedbackEmail() { + mainStore?.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 8b3e444c59..a18e07daef 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -8,7 +8,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras @@ -17,21 +17,21 @@ import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result -import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.externallinkprovider.TxExploreState import com.tangem.blockchain.network.ResultChecker +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.userWalletsListManager import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -42,6 +42,7 @@ class TransactionManagerImpl( private val appStateHolder: AppStateHolder, private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, + private val userWalletsListManager: UserWalletsListManager, ) : TransactionManager { override suspend fun sendApproveTransaction( @@ -140,7 +141,7 @@ class TransactionManagerImpl( Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } Blockchain.Cosmos -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) + Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) else -> null } @@ -223,26 +224,33 @@ class TransactionManagerImpl( when (fee.data) { is TransactionFee.Single -> { val normalFee = (fee.data as TransactionFee.Single).normal - val singleFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - ) - ProxyFees.SingleFee( - singleFee = singleFee, - ) + val singleFee = if (normalFee as? Fee.CardanoToken != null) { + ProxyFee.CardanoToken( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + minAdaValue = normalFee.minAdaValue, + ) + } else { + ProxyFee.Common( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + ) + } + + ProxyFees.SingleFee(singleFee = singleFee) } is TransactionFee.Choosable -> { val choosableFee = fee.data as TransactionFee.Choosable ProxyFees.MultipleFees( - minFee = ProxyFee( + minFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ), - normalFee = ProxyFee( + normalFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ), - priorityFee = ProxyFee( + priorityFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ), @@ -299,15 +307,15 @@ class TransactionManagerImpl( is Result.Success -> { val choosableFee = fee.data - val minProxyFee = ProxyFee( + val minProxyFee = ProxyFee.Common( gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) - val normalProxyFee = ProxyFee( + val normalProxyFee = ProxyFee.Common( gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) - val priorityProxyFee = ProxyFee( + val priorityProxyFee = ProxyFee.Common( gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) @@ -355,12 +363,12 @@ class TransactionManagerImpl( } } - private fun handleSendResult(result: SimpleResult): SendTxResult { + private fun handleSendResult(result: Result): SendTxResult { when (result) { - is SimpleResult.Success -> { + is Result.Success -> { return SendTxResult.Success } - is SimpleResult.Failure -> { + is Result.Failure -> { if (ResultChecker.isNetworkError(result)) return SendTxResult.NetworkError(result.error) val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError() when (error) { @@ -462,7 +470,7 @@ class TransactionManagerImpl( scale = blockchain.decimals(), mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), ) - val minFee = ProxyFee( + val minFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, @@ -470,7 +478,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - val normalFee = ProxyFee( + val normalFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, @@ -478,7 +486,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - val priorityFee = ProxyFee( + val priorityFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 8d098f79c1..ac5a55104e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -2,85 +2,20 @@ package com.tangem.tap.proxy import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.Currency -import com.tangem.lib.crypto.models.Currency.NativeToken -import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.ProxyAmount -import com.tangem.tap.userWalletsListManager import timber.log.Timber import java.math.BigDecimal class UserWalletManagerImpl( private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListManager: UserWalletsListManager, ) : UserWalletManager { - override suspend fun getUserTokens( - networkId: String, - derivationPath: String?, - isExcludeCustom: Boolean, - ): List { - // FIXME: Find user wallet by ID - val userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) { - "No user wallet selected" - } - return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWallet.walletId) - .filter { - val checkCustom = if (isExcludeCustom) { - !it.isCustom - } else { - true - } - val blockchain = Blockchain.fromId(it.network.id.value) - - blockchain.toNetworkId() == networkId && - checkCustom && - it.network.derivationPath.value == derivationPath - } - .map { - val blockchain = Blockchain.fromId(it.network.id.value) - - if (it is CryptoCurrency.Token) { - NonNativeToken( - id = it.id.rawCurrencyId ?: "", - name = it.name, - symbol = it.symbol, - networkId = blockchain.toNetworkId(), - contractAddress = it.contractAddress, - decimalCount = it.decimals, - ) - } else { - NativeToken( - id = it.id.rawCurrencyId ?: "", - name = it.name, - symbol = it.symbol, - networkId = blockchain.toNetworkId(), - ) - } - } - } - - override fun getNativeTokenForNetwork(networkId: String): Currency { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - return NativeToken( - id = blockchain.toCoinId(), - name = blockchain.fullName, - symbol = blockchain.currency, - networkId = networkId, - ) - } - override fun getWalletId(): String { val selectedUserWallet = requireNotNull( userWalletsListManager.selectedUserWalletSync, @@ -88,18 +23,6 @@ class UserWalletManagerImpl( return selectedUserWallet.walletId.stringValue } - override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean { - val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" } - return try { - val walletManager = getActualWalletManager(blockchain, derivationPath) - walletManager.cardTokens.any { - it.id == currency.id - } - } catch (e: IllegalArgumentException) { - false - } - } - override suspend fun hideAllTokens() { // FIXME: Used only in Tester Actions Timber.w("Not implemented") @@ -119,37 +42,6 @@ class UserWalletManagerImpl( ?.hash } - override suspend fun getCurrentWalletTokensBalance( - networkId: String, - extraTokens: List, - derivationPath: String?, - ): Map { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - - // workaround for get balance for tokens that doesn't exist in wallet - val extraTokensToLoadBalance = extraTokens - .filterIsInstance() - .map { - it.toSdkToken() - } - .filter { - !walletManager.cardTokens.contains(it) - } - walletManager.addTokens(extraTokensToLoadBalance) - walletManager.update() - val balances = walletManager.wallet.amounts.map { entry -> - val amount = entry.value - amount.currencySymbol to ProxyAmount( - amount.currencySymbol, - amount.value ?: BigDecimal.ZERO, - amount.decimals, - ) - }.toMap() - extraTokensToLoadBalance.forEach { walletManager.removeToken(it) } - return balances - } - override suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) @@ -179,14 +71,4 @@ class UserWalletManagerImpl( "No wallet manager found" } } -} - -private fun NonNativeToken.toSdkToken(): Token { - return Token( - id = this.id, - name = this.name, - symbol = this.symbol, - contractAddress = this.contractAddress, - decimals = this.decimalCount, - ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index d3f44858b1..37fc1060b4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,9 +1,8 @@ package com.tangem.tap.proxy.di -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.tap.proxy.* @@ -28,13 +27,11 @@ internal object ProxyModule { @Singleton fun provideUserWalletManager( walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, - userWalletsStore: UserWalletsStore, + userWalletsListManager: UserWalletsListManager, ): UserWalletManager { return UserWalletManagerImpl( walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, - userWalletsStore = userWalletsStore, + userWalletsListManager = userWalletsListManager, ) } @@ -44,11 +41,13 @@ internal object ProxyModule { appStateHolder: AppStateHolder, cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, + userWalletsListManager: UserWalletsListManager, ): TransactionManager { return TransactionManagerImpl( appStateHolder = appStateHolder, cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, + userWalletsListManager = userWalletsListManager, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 995658a42c..f854ba0dae 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,8 +1,9 @@ package com.tangem.tap.proxy.redux -import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter @@ -23,5 +24,6 @@ sealed interface DaggerGraphAction : Action { val cardSdkConfigRepository: CardSdkConfigRepository, val sendRouter: SendRouter, val qrScanningRouter: QrScanningRouter, + val emailSender: EmailSender, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index 7074523df0..b25fdce12b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -22,6 +22,7 @@ object DaggerGraphReducer { cardSdkConfigRepository = action.cardSdkConfigRepository, sendRouter = action.sendRouter, qrScanningRouter = action.qrScanningRouter, + emailSender = action.emailSender, ) } } 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 c23def20bc..a61bdcde57 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 @@ -1,9 +1,9 @@ package com.tangem.tap.proxy.redux import com.tangem.TangemSdkLogger -import com.tangem.blockchain.common.AccountCreator -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.core.navigation.email.EmailSender +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -13,15 +13,19 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.features.details.DetailsEntryPoint +import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -30,7 +34,7 @@ import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder @@ -42,7 +46,7 @@ data class DaggerGraphState( val customTokenFeatureToggles: CustomTokenFeatureToggles? = null, val scanCardUseCase: ScanCardUseCase? = null, val walletRouter: WalletRouter? = null, - val walletConnectRepository: WalletConnectRepository? = null, + val walletConnectRepository: LegacyWalletConnectRepository? = null, val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, @@ -61,14 +65,18 @@ data class DaggerGraphState( val sendRouter: SendRouter? = null, val qrScanningRouter: QrScanningRouter? = null, val currenciesRepository: CurrenciesRepository? = null, - val blockchainDataStorage: BlockchainDataStorage? = null, - val accountCreator: AccountCreator? = null, - val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles? = null, val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, + val generateWalletNameUseCase: GenerateWalletNameUseCase? = null, val cardRepository: CardRepository? = null, val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null, val tangemSdkLogger: TangemSdkLogger? = null, - val blockchainSDKLogger: BlockchainSDKLogger? = null, + val settingsRepository: SettingsRepository? = null, + val blockchainSDKFactory: BlockchainSDKFactory? = null, + val emailSender: EmailSender? = null, + val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, + val assetLoader: AssetLoader? = null, + val detailsFeatureToggles: DetailsFeatureToggles? = null, + val detailsEntryPoint: DetailsEntryPoint? = null, ) : StateType \ No newline at end of file diff --git a/app/src/mocked/res/values/strings.xml b/app/src/mocked/res/values/strings.xml new file mode 100644 index 0000000000..f13e8b7c5e --- /dev/null +++ b/app/src/mocked/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + Mocked Tangem + + diff --git a/app/src/mocked/res/xml/network_security_config.xml b/app/src/mocked/res/xml/network_security_config.xml new file mode 100644 index 0000000000..52c44ac992 --- /dev/null +++ b/app/src/mocked/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt index 43c27150f4..aadeccf980 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt @@ -67,6 +67,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { ), isTestnet = false, standardType = Network.StandardType.ERC20, + hasFiatFeeRate = true, ), name = "NEVER-MIND", symbol = "NEVER-MIND", diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index b1e1189c8e..fdf0ed8db2 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -10,7 +10,7 @@ import com.tangem.domain.common.configs.MultiWalletCardConfig import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest @@ -21,7 +21,7 @@ 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, diff --git a/build.gradle.kts b/build.gradle.kts index 8ceb8869c1..d92b770060 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(deps.plugins.hilt.android) apply false alias(deps.plugins.google.services) apply false alias(deps.plugins.firebase.crashlytics) apply false + alias(deps.plugins.room) apply false } val clean by tasks.registering { 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 80517d7175..b77078a426 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -3,46 +3,46 @@ package com.tangem.core.analytics.models sealed class AnalyticsParam { sealed class CardBalanceState(val value: String) { - object Empty : CardBalanceState("Empty") - object Full : CardBalanceState("Full") - object CustomToken : CardBalanceState("Custom Token") - object BlockchainError : CardBalanceState("Blockchain Error") - object NoRate : CardBalanceState("No Rate") + data object Empty : CardBalanceState("Empty") + data object Full : CardBalanceState("Full") + data object CustomToken : CardBalanceState("Custom Token") + data object BlockchainError : CardBalanceState("Blockchain Error") + data object NoRate : CardBalanceState("No Rate") companion object } sealed class TokenBalanceState(val value: String) { - object Empty : TokenBalanceState("Empty") - object Full : TokenBalanceState("Full") + data object Empty : TokenBalanceState("Empty") + data object Full : TokenBalanceState("Full") } sealed class RateApp(val value: String) { - object Liked : RateApp("Liked") - object Disliked : RateApp("Disliked") - object Closed : RateApp("Close") + data object Liked : RateApp("Liked") + data object Disliked : RateApp("Disliked") + data object Closed : RateApp("Close") } sealed class OnOffState(val value: String) { - object On : OnOffState("On") - object Off : OnOffState("Off") + data object On : OnOffState("On") + data object Off : OnOffState("Off") } sealed class OrganizeSortType(val value: String) { - object ByBalance : OrganizeSortType("By Balance") - object Manually : OrganizeSortType("Manually") + data object ByBalance : OrganizeSortType("By Balance") + data object Manually : OrganizeSortType("Manually") } sealed class UserCode(val value: String) { - object AccessCode : UserCode("Access Code") - object Passcode : UserCode("Passcode") + data object AccessCode : UserCode("Access Code") + data object Passcode : UserCode("Passcode") } sealed class AccessCodeRecoveryStatus(val value: String) { val key: String = "Status" - object Enabled : AccessCodeRecoveryStatus("Enabled") - object Disabled : AccessCodeRecoveryStatus("Disabled") + data object Enabled : AccessCodeRecoveryStatus("Enabled") + data object Disabled : AccessCodeRecoveryStatus("Disabled") companion object { fun from(enabled: Boolean): AccessCodeRecoveryStatus { @@ -52,9 +52,9 @@ sealed class AnalyticsParam { } sealed class Error(val value: String) { - object App : Error("App Error") - object CardSdk : Error("Card Sdk Error") - object BlockchainSdk : Error("Blockchain Sdk Error") + data object App : Error("App Error") + data object CardSdk : Error("Card Sdk Error") + data object BlockchainSdk : Error("Blockchain Sdk Error") } sealed class ScreensSources(val value: String) { @@ -86,8 +86,8 @@ sealed class AnalyticsParam { val permissionType: String, ) : TxSentFrom("Approve"), TxData - object WalletConnect : TxSentFrom("WalletConnect") - object Sell : TxSentFrom("Sell") + data object WalletConnect : TxSentFrom("WalletConnect") + data object Sell : TxSentFrom("Sell") } sealed interface TxData { @@ -117,13 +117,13 @@ sealed class AnalyticsParam { } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType("Private key") - object NewSeed : WalletCreationType("New seed") - object SeedImport : WalletCreationType("Seed import") + data object PrivateKey : WalletCreationType("Private key") + data object NewSeed : WalletCreationType("New seed") + data object SeedImport : WalletCreationType("Seed import") } sealed class WalletType(val value: String) { - object MultiCurrency : WalletType(value = "Multicurrency") + data object MultiCurrency : WalletType(value = "Multicurrency") class SingleCurrency(currencyName: String) : WalletType(currencyName) } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index e4239e40d6..4deb2329d1 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -3,11 +3,16 @@ plugins { alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) + alias(deps.plugins.room) id("configuration") } android { namespace = "com.tangem.datasource" + + room { + schemaDirectory("$projectDir/schemas") + } } dependencies { @@ -39,6 +44,7 @@ dependencies { /** Network */ implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(deps.moshi.adapters) implementation(deps.okHttp) implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) @@ -53,10 +59,19 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) + mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) /** Local storages */ implementation(deps.androidx.datastore) + implementation(deps.room.runtime) + implementation(deps.room.ktx) + kapt(deps.room.compiler) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/core/datasource/schemas/com.tangem.datasource.local.db.TangemDatabase/1.json b/core/datasource/schemas/com.tangem.datasource.local.db.TangemDatabase/1.json new file mode 100644 index 0000000000..3efb2a6a01 --- /dev/null +++ b/core/datasource/schemas/com.tangem.datasource.local.db.TangemDatabase/1.json @@ -0,0 +1,304 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "a8a710af25033ee27e5043d001385234", + "entities": [ + { + "tableName": "UserWalletEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `artworkUrl` TEXT NOT NULL, `isMultiCurrency` INTEGER NOT NULL, `hasBackupError` INTEGER NOT NULL, `cardsInWallet` TEXT NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artworkUrl", + "columnName": "artworkUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isMultiCurrency", + "columnName": "isMultiCurrency", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasBackupError", + "columnName": "hasBackupError", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cardsInWallet", + "columnName": "cardsInWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ordinalNumber", + "columnName": "ordinalNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_UserWalletEntity_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_UserWalletEntity_ordinalNumber", + "unique": true, + "columnNames": [ + "ordinalNumber" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_ordinalNumber` ON `${TABLE_NAME}` (`ordinalNumber`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "CryptoCurrenciesAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `title` TEXT NOT NULL, `currenciesCount` INTEGER NOT NULL, `isArchived` INTEGER NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userWalletId", + "columnName": "userWalletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currenciesCount", + "columnName": "currenciesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isArchived", + "columnName": "isArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ordinalNumber", + "columnName": "ordinalNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CryptoCurrenciesAccountEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_CryptoCurrenciesAccountEntity_userWalletId", + "unique": false, + "columnNames": [ + "userWalletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)" + } + ], + "foreignKeys": [ + { + "table": "UserWalletEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userWalletId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CryptoCurrencyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `currencyBackendId` TEXT, `networkId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `contractAddress` TEXT, `derivationPath` TEXT, FOREIGN KEY(`accountId`) REFERENCES `CryptoCurrenciesAccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currencyBackendId", + "columnName": "currencyBackendId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "networkId", + "columnName": "networkId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userWalletId", + "columnName": "userWalletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "decimals", + "columnName": "decimals", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contractAddress", + "columnName": "contractAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "derivationPath", + "columnName": "derivationPath", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CryptoCurrencyEntity_currencyBackendId", + "unique": false, + "columnNames": [ + "currencyBackendId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_currencyBackendId` ON `${TABLE_NAME}` (`currencyBackendId`)" + }, + { + "name": "index_CryptoCurrencyEntity_networkId", + "unique": false, + "columnNames": [ + "networkId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_networkId` ON `${TABLE_NAME}` (`networkId`)" + }, + { + "name": "index_CryptoCurrencyEntity_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_CryptoCurrencyEntity_userWalletId", + "unique": false, + "columnNames": [ + "userWalletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)" + } + ], + "foreignKeys": [ + { + "table": "CryptoCurrenciesAccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserWalletEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userWalletId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a8a710af25033ee27e5043d001385234')" + ] + } +} diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt similarity index 82% rename from libs/auth/src/main/java/com/tangem/lib/auth/AuthProvider.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index 1323079ec6..a11b7bf574 100644 --- a/libs/auth/src/main/java/com/tangem/lib/auth/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.lib.auth +package com.tangem.datasource.api.common /** * Provides auth for tangemTech API diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt index 510ac95179..1dd6c425a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter import com.tangem.common.json.TangemSdkAdapter +import com.tangem.datasource.api.common.adapter.BigDecimalAdapter import retrofit2.converter.moshi.MoshiConverterFactory /** diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigDecimalAdapter.kt similarity index 85% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigDecimalAdapter.kt index e81aa15e06..c025caee95 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/BigDecimalAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigDecimalAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.common +package com.tangem.datasource.api.common.adapter import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt similarity index 91% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt index 38f4b81532..5e637ed8c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/DateTimeAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/DateTimeAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.common +package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import org.joda.time.DateTime diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt index 95978206f2..6a7cd420c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/LocalDateAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.common +package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import org.joda.time.LocalDate diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt new file mode 100644 index 0000000000..f61191a104 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.api.common.adapter + +import com.squareup.moshi.* +import com.squareup.moshi.adapters.EnumJsonAdapter + +/** + * Object to create a adapter for enum types with support for unknown enum values. + */ +object UnknownEnumMoshiAdapter { + + fun > create(enumType: Class, defaultValue: T): JsonAdapter = + EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index b69ef4e355..52a2e4cc6f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -33,12 +33,8 @@ internal class ApiResponseCallDelegate( } override fun onFailure(call: Call, t: Throwable) { - val e = if (t.isNetworkException()) { - ApiResponseError.NetworkException - } else { - ApiResponseError.UnknownException(t) - } - val safeResponse = apiError(e) + val error = t.toApiError() + val safeResponse = apiError(error) responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index e342289f0c..cfd2146441 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -73,7 +73,10 @@ sealed class ApiResponseError : Exception() { } /** Represents a network error, typically when there's no connectivity. */ - object NetworkException : ApiResponseError() + data object NetworkException : ApiResponseError() + + /** Represents a timeout error, typically when the server takes too long to respond. */ + data object TimeoutException : ApiResponseError() /** * Represents an unexpected exception that doesn't fall into one of the other categories. diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index 718392b0d9..92a5af0ac5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.api.common.response +import kotlinx.coroutines.TimeoutCancellationException import retrofit2.Response import java.net.ConnectException +import java.net.SocketTimeoutException import java.net.UnknownHostException +import java.util.concurrent.TimeoutException import javax.net.ssl.SSLHandshakeException internal fun Response.toSafeApiResponse(): ApiResponse { @@ -23,10 +26,14 @@ internal fun Response.toSafeApiResponse(): ApiResponse { } } -internal fun Throwable.isNetworkException(): Boolean = when (this) { +internal fun Throwable.toApiError(): ApiResponseError = when (this) { is ConnectException, is UnknownHostException, is SSLHandshakeException, - -> true - else -> false + -> ApiResponseError.NetworkException + is TimeoutException, + is TimeoutCancellationException, + is SocketTimeoutException, + -> ApiResponseError.TimeoutException + else -> ApiResponseError.UnknownException(cause = this) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b7055574a3..54a7935d66 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.express import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.* import retrofit2.http.Body @@ -42,6 +43,7 @@ interface TangemExpressApi { @Query("fromContractAddress") fromContractAddress: String, @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, + @Query("fromAddress") fromAddress: String, @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, @Query("fromDecimals") fromDecimals: Int, @@ -56,4 +58,7 @@ interface TangemExpressApi { @GET("exchange-status") suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse + + @POST("exchange-sent") + suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt new file mode 100644 index 0000000000..b585a80775 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/ExchangeSentRequestBody.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.api.express.models.request + +import com.squareup.moshi.Json + +data class ExchangeSentRequestBody( + @Json(name = "txId") + val txId: String, + @Json(name = "fromNetwork") + val fromNetwork: String, + @Json(name = "fromAddress") + val fromAddress: String, + @Json(name = "payinAddress") + val payinAddress: String, + @Json(name = "payinExtraId") + val payinExtraId: String?, + @Json(name = "txHash") + val txHash: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt new file mode 100644 index 0000000000..b6b1453465 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeSentResponseBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json + +data class ExchangeSentResponseBody( + @Json(name = "txId") + val txId: String, + @Json(name = "status") + val status: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index 6af6ee2409..a269f45c23 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -10,8 +10,8 @@ data class ExchangeStatusResponse( @Json(name = "externalTxId") val externalTxId: String, - @Json(name = "externalTxStatus") - val externalStatus: ExchangeStatus, + @Json(name = "status") + val status: ExchangeStatus, @Json(name = "externalTxUrl") val externalTxUrl: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt new file mode 100644 index 0000000000..c7d564c69a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -0,0 +1,48 @@ +package com.tangem.datasource.api.stakekit + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.request.RevenueOption +import com.tangem.datasource.api.stakekit.models.request.YieldType +import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield +import com.tangem.datasource.api.stakekit.models.response.model.Yield +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +@Suppress("LongParameterList") +interface StakeKitApi { + + @GET("yields/enabled") + suspend fun getMultipleYields( + @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean, + @Query("type") type: YieldType, + @Query("revenueOption") revenueOption: RevenueOption, + @Query("page") page: Int, + @Query("network") network: String, + @Query("limit") limit: Int, + ): ApiResponse + + @GET("yields/{integrationId}") + suspend fun getSingleYield( + @Path("integrationId") integrationId: String, + @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false, + ): ApiResponse + + @GET("yields/balances") + suspend fun getMultipleYieldBalances( + @Body body: List, + ): ApiResponse> + + @GET("yields/{integrationId}/balances") + suspend fun getSingleYieldBalance( + @Path("integrationId") integrationId: String, + @Body body: YieldBalanceRequestBody, + ): ApiResponse + + @GET("tokens") + suspend fun getTokens(): ApiResponse> +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt new file mode 100644 index 0000000000..1872f19673 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/MultipleYieldBalancesRequestBody.kt @@ -0,0 +1,31 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json + +data class MultipleYieldBalancesRequestBody( + @Json(name = "addresses") val addresses: Address, + @Json(name = "args") val args: MultipleYieldBalancesRequestArgs, + @Json(name = "integrationId") val integrationId: String, +) { + + data class Address( + @Json(name = "address") val address: String, + @Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null, + @Json(name = "explorerUrl") val explorerUrl: String, + ) { + + data class AdditionalAddresses( + @Json(name = "cosmosPubKey") val cosmosPubKey: String? = null, + @Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null, + @Json(name = "stakeAccounts") val stakeAccounts: List? = null, + @Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List? = null, + @Json(name = "tezosPubKey") val tezosPubKey: String? = null, + @Json(name = "cAddressBech") val cAddressBech: String? = null, + @Json(name = "pAddressBech") val pAddressBech: String? = null, + ) + } + + data class MultipleYieldBalancesRequestArgs( + @Json(name = "validatorAddresses") val validatorAddresses: List, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt new file mode 100644 index 0000000000..e396cd7ebb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/RevenueOption.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.stakekit.models.request + +enum class RevenueOption(val value: String) { + SUPPORTS_FEE("supportsFee"), + SUPPORTS_REV_SHARE("supportsRevShare"), + ; + + override fun toString(): String { + return value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt new file mode 100644 index 0000000000..7433386e73 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -0,0 +1,31 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json + +data class YieldBalanceRequestBody( + @Json(name = "addresses") val addresses: Address, + @Json(name = "args") val args: YieldBalanceRequestArgs, + @Json(name = "integrationId") val integrationId: String? = null, +) { + + data class Address( + @Json(name = "address") val address: String, + @Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null, + @Json(name = "explorerUrl") val explorerUrl: String, + ) { + + data class AdditionalAddresses( + @Json(name = "cosmosPubKey") val cosmosPubKey: String? = null, + @Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null, + @Json(name = "stakeAccounts") val stakeAccounts: List? = null, + @Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List? = null, + @Json(name = "tezosPubKey") val tezosPubKey: String? = null, + @Json(name = "cAddressBech") val cAddressBech: String? = null, + @Json(name = "pAddressBech") val pAddressBech: String? = null, + ) + } + + data class YieldBalanceRequestArgs( + @Json(name = "validatorAddresses") val validatorAddresses: List, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldType.kt new file mode 100644 index 0000000000..5eb7182cb5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldType.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.stakekit.models.request + +enum class YieldType(private val value: String) { + STAKING("staking"), + LIQUID_STAKING("liquid-staking"), + LENDING("lending"), + RESTAKING("restaking"), + VAULT("vault"), + ; + + override fun toString(): String { + return value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt new file mode 100644 index 0000000000..c4a958c435 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.stakekit.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.Yield + +@JsonClass(generateAdapter = true) +data class EnabledYieldsResponse( + @Json(name = "data") + val data: List, + @Json(name = "hasNextPage") + val hasNextPage: Boolean, + @Json(name = "limit") + val limit: Int, + @Json(name = "page") + val page: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt new file mode 100644 index 0000000000..301b7b5805 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AddressArgument( + @Json(name = "required") + val required: Boolean, + @Json(name = "network") + val network: String? = null, + @Json(name = "minimum") + val minimum: Int? = null, + @Json(name = "maximum") + val maximum: Int? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt new file mode 100644 index 0000000000..5e60da84d9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt @@ -0,0 +1,50 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json + +enum class StakingActionType { + @Json(name = "STAKE") + STAKE, + + @Json(name = "UNSTAKE") + UNSTAKE, + + @Json(name = "CLAIM_REWARDS") + CLAIM_REWARDS, + + @Json(name = "RESTAKE_REWARDS") + RESTAKE_REWARDS, + + @Json(name = "WITHDRAW") + WITHDRAW, + + @Json(name = "RESTAKE") + RESTAKE, + + @Json(name = "CLAIM_UNSTAKED") + CLAIM_UNSTAKED, + + @Json(name = "UNLOCK_LOCKED") + UNLOCK_LOCKED, + + @Json(name = "STAKE_LOCKED") + STAKE_LOCKED, + + @Json(name = "VOTE") + VOTE, + + @Json(name = "REVOKE") + REVOKE, + + @Json(name = "VOTE_LOCKED") + VOTE_LOCKED, + + @Json(name = "REVOTE") + REVOTE, + + @Json(name = "REBOND") + REBOND, + + @Json(name = "MIGRATE") + MIGRATE, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt new file mode 100644 index 0000000000..88d5d12b59 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt @@ -0,0 +1,218 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Token( + @Json(name = "name") val name: String, + @Json(name = "network") val network: NetworkType, + @Json(name = "symbol") val symbol: String, + @Json(name = "decimals") val decimals: Int, + @Json(name = "address") val address: String?, + @Json(name = "coinGeckoId") val coinGeckoId: String?, + @Json(name = "logoURI") val logoURI: String?, + @Json(name = "isPoints") val isPoints: Boolean?, +) { + enum class NetworkType { + @Json(name = "avalanche-c") + AVALANCHE_C, + + @Json(name = "avalanche-atomic") + AVALANCHE_ATOMIC, + + @Json(name = "avalanche-p") + AVALANCHE_P, + + @Json(name = "arbitrum") + ARBITRUM, + + @Json(name = "binance") + BINANCE, + + @Json(name = "celo") + CELO, + + @Json(name = "ethereum") + ETHEREUM, + + @Json(name = "ethereum-goerli") + ETHEREUM_GOERLI, + + @Json(name = "ethereum-holesky") + ETHEREUM_HOLESKY, + + @Json(name = "fantom") + FANTOM, + + @Json(name = "harmony") + HARMONY, + + @Json(name = "optimism") + OPTIMISM, + + @Json(name = "polygon") + POLYGON, + + @Json(name = "gnosis") + GNOSIS, + + @Json(name = "moonriver") + MOONRIVER, + + @Json(name = "okc") + OKC, + + @Json(name = "zksync") + ZKSYNC, + + @Json(name = "viction") + VICTION, + + @Json(name = "agoric") + AGORIC, + + @Json(name = "akash") + AKASH, + + @Json(name = "axelar") + AXELAR, + + @Json(name = "band-protocol") + BAND_PROTOCOL, + + @Json(name = "bitsong") + BITSONG, + + @Json(name = "canto") + CANTO, + + @Json(name = "chihuahua") + CHIHUAHUA, + + @Json(name = "comdex") + COMDEX, + + @Json(name = "coreum") + COREUM, + + @Json(name = "cosmos") + COSMOS, + + @Json(name = "crescent") + CRESCENT, + + @Json(name = "cronos") + CRONOS, + + @Json(name = "cudos") + CUDOS, + + @Json(name = "desmos") + DESMOS, + + @Json(name = "dydx") + DYDX, + + @Json(name = "evmos") + EVMOS, + + @Json(name = "fetch-ai") + FETCH_AI, + + @Json(name = "gravity-bridge") + GRAVITY_BRIDGE, + + @Json(name = "injective") + INJECTIVE, + + @Json(name = "irisnet") + IRISNET, + + @Json(name = "juno") + JUNO, + + @Json(name = "kava") + KAVA, + + @Json(name = "ki-network") + KI_NETWORK, + + @Json(name = "mars-protocol") + MARS_PROTOCOL, + + @Json(name = "nym") + NYM, + + @Json(name = "okex-chain") + OKEX_CHAIN, + + @Json(name = "onomy") + ONOMY, + + @Json(name = "osmosis") + OSMOSIS, + + @Json(name = "persistence") + PERSISTENCE, + + @Json(name = "quicksilver") + QUICKSILVER, + + @Json(name = "regen") + REGEN, + + @Json(name = "secret") + SECRET, + + @Json(name = "sentinel") + SENTINEL, + + @Json(name = "sommelier") + SOMMELIER, + + @Json(name = "stafi") + STAFI, + + @Json(name = "stargaze") + STARGAZE, + + @Json(name = "stride") + STRIDE, + + @Json(name = "teritori") + TERITORI, + + @Json(name = "tgrade") + TGRADE, + + @Json(name = "umee") + UMEE, + + @Json(name = "polkadot") + POLKADOT, + + @Json(name = "kusama") + KUSAMA, + + @Json(name = "westend") + WESTEND, + + @Json(name = "binancebeacon") + BINANCEBEACON, + + @Json(name = "near") + NEAR, + + @Json(name = "solana") + SOLANA, + + @Json(name = "tezos") + TEZOS, + + @Json(name = "tron") + TRON, + + UNKNOWN, + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt new file mode 100644 index 0000000000..2bc30185af --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TokenWithYield( + @Json(name = "token") val token: Token, + @Json(name = "availableYields") val availableYieldIds: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt new file mode 100644 index 0000000000..c122022402 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt @@ -0,0 +1,146 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class Yield( + @Json(name = "id") + val id: String, + @Json(name = "token") + val token: Token, + @Json(name = "tokens") + val tokens: List, + @Json(name = "args") + val args: Args, + @Json(name = "status") + val status: Status, + @Json(name = "apy") + val apy: BigDecimal, + @Json(name = "rewardRate") + val rewardRate: Double, + @Json(name = "rewardType") + val rewardType: RewardType, + @Json(name = "metadata") + val metadata: Metadata, + @Json(name = "validators") + val validators: List, + @Json(name = "isAvailable") + val isAvailable: Boolean, +) { + + @JsonClass(generateAdapter = true) + data class Status( + @Json(name = "enter") + val enter: Boolean, + @Json(name = "exit") + val exit: Boolean?, + ) + + @JsonClass(generateAdapter = true) + data class Args( + @Json(name = "enter") + val enter: Enter, + @Json(name = "exit") + val exit: Enter?, + ) { + @JsonClass(generateAdapter = true) + data class Enter( + @Json(name = "addresses") + val addresses: Addresses, + @Json(name = "args") + val args: Map, + ) { + @JsonClass(generateAdapter = true) + data class Addresses( + @Json(name = "address") + val address: AddressArgument, + @Json(name = "additionalAddresses") + val additionalAddresses: Map? = null, + ) + } + } + + @JsonClass(generateAdapter = true) + data class Validator( + @Json(name = "address") + val address: String, + @Json(name = "status") + val status: String, + @Json(name = "name") + val name: String, + @Json(name = "image") + val image: String?, + @Json(name = "website") + val website: String?, + @Json(name = "apr") + val apr: Double?, + @Json(name = "commission") + val commission: Double?, + @Json(name = "stakedBalance") + val stakedBalance: String?, + @Json(name = "votingPower") + val votingPower: Double?, + @Json(name = "preferred") + val preferred: Boolean, + ) + + @JsonClass(generateAdapter = true) + data class Metadata( + @Json(name = "name") + val name: String, + @Json(name = "logoURI") + val logoUri: String, + @Json(name = "description") + val description: String, + @Json(name = "documentation") + val documentation: String?, + @Json(name = "gasFeeToken") + val gasFeeToken: Token, + @Json(name = "token") + val token: Token, + @Json(name = "tokens") + val tokens: List, + @Json(name = "type") + val type: String, + @Json(name = "rewardSchedule") + val rewardSchedule: String, + @Json(name = "cooldownPeriod") + val cooldownPeriod: Period, + @Json(name = "warmupPeriod") + val warmupPeriod: Period, + @Json(name = "rewardClaiming") + val rewardClaiming: String, + @Json(name = "defaultValidator") + val defaultValidator: String?, + @Json(name = "minimumStake") + val minimumStake: Int?, + @Json(name = "supportsMultipleValidators") + val supportsMultipleValidators: Boolean, + @Json(name = "revshare") + val revshare: Enabled, + @Json(name = "fee") + val fee: Enabled, + ) { + + @JsonClass(generateAdapter = true) + data class Period( + @Json(name = "days") + val days: Int, + ) + + @JsonClass(generateAdapter = true) + data class Enabled( + @Json(name = "enabled") + val enabled: Boolean, + ) + } + + enum class RewardType { + @Json(name = "apy") + APY, // auto + @Json(name = "apr") + APR, // manual + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt new file mode 100644 index 0000000000..01f0e26c16 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt @@ -0,0 +1,138 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.joda.time.DateTime +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class YieldBalanceWrapper( + @Json(name = "balances") + val balances: List, + @Json(name = "integrationId") + val integrationId: String?, +) { + + @JsonClass(generateAdapter = true) + data class Balance( + @Json(name = "groupId") + val groupId: String, + @Json(name = "type") + val type: BalanceType, + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "date") + val date: DateTime?, + @Json(name = "pricePerShare") + val pricePerShare: BigDecimal, + @Json(name = "pendingActions") + val pendingActions: List, + @Json(name = "token") + val token: Token, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @Json(name = "providerId") + val providerId: String?, + ) { + + enum class BalanceType { + @Json(name = "available") + AVAILABLE, + + @Json(name = "staked") + STAKED, + + @Json(name = "unstaking") + UNSTAKING, + + @Json(name = "unstaked") + UNSTAKED, + + @Json(name = "preparing") + PREPARING, + + @Json(name = "rewards") + REWARDS, + + @Json(name = "locked") + LOCKED, + + @Json(name = "unlocking") + UNLOCKING, + } + + @JsonClass(generateAdapter = true) + data class PendingAction( + @Json(name = "type") + val type: StakingActionType, + @Json(name = "passthrough") + val passthrough: String, + @Json(name = "args") + val args: PendingActionArgs?, + ) { + @JsonClass(generateAdapter = true) + data class PendingActionArgs( + @Json(name = "amount") + val amount: Amount?, + @Json(name = "duration") + val duration: Duration?, + @Json(name = "validatorAddress") + val validatorAddress: Required?, + @Json(name = "validatorAddresses") + val validatorAddresses: Required?, + @Json(name = "nfts") + val nfts: List?, + @Json(name = "tronResource") + val tronResource: TronResource?, + @Json(name = "signatureVerification") + val signatureVerification: Required?, + ) { + @JsonClass(generateAdapter = true) + data class Amount( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: BigDecimal?, + @Json(name = "maximum") + val maximum: BigDecimal?, + ) + + @JsonClass(generateAdapter = true) + data class Duration( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: Int?, + @Json(name = "maximum") + val maximum: Int?, + ) + + @JsonClass(generateAdapter = true) + data class Nft( + @Json(name = "baycId") + val baycId: Required?, + @Json(name = "maycId") + val maycId: Required?, + @Json(name = "bakcId") + val bakcId: Required?, + ) + + @JsonClass(generateAdapter = true) + data class TronResource( + @Json(name = "required") + val required: Boolean, + @Json(name = "options") + val options: List, + ) + } + } + + @JsonClass(generateAdapter = true) + data class Required( + @Json(name = "required") + val required: Boolean, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt new file mode 100644 index 0000000000..98637444a4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt @@ -0,0 +1,138 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.joda.time.DateTime +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class YieldBalances( + @Json(name = "balances") + val balances: List, + @Json(name = "integrationId") + val integrationId: String, +) { + + @JsonClass(generateAdapter = true) + data class Balance( + @Json(name = "groupId") + val groupId: String, + @Json(name = "type") + val type: BalanceType, + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "date") + val date: DateTime?, + @Json(name = "pricePerShare") + val pricePerShare: BigDecimal, + @Json(name = "pendingActions") + val pendingActions: List, + @Json(name = "token") + val token: Token, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @Json(name = "providerId") + val providerId: String?, + ) { + + enum class BalanceType { + @Json(name = "available") + AVAILABLE, + + @Json(name = "staked") + STAKED, + + @Json(name = "unstaking") + UNSTAKING, + + @Json(name = "unstaked") + UNSTAKED, + + @Json(name = "preparing") + PREPARING, + + @Json(name = "rewards") + REWARDS, + + @Json(name = "locked") + LOCKED, + + @Json(name = "unlocking") + UNLOCKING, + } + + @JsonClass(generateAdapter = true) + data class PendingAction( + @Json(name = "type") + val type: StakingActionType, + @Json(name = "passthrough") + val passthrough: String, + @Json(name = "args") + val args: PendingActionArgs?, + ) { + @JsonClass(generateAdapter = true) + data class PendingActionArgs( + @Json(name = "amount") + val amount: Amount?, + @Json(name = "duration") + val duration: Duration?, + @Json(name = "validatorAddress") + val validatorAddress: Required?, + @Json(name = "validatorAddresses") + val validatorAddresses: Required?, + @Json(name = "nfts") + val nfts: List?, + @Json(name = "tronResource") + val tronResource: TronResource?, + @Json(name = "signatureVerification") + val signatureVerification: Required?, + ) { + @JsonClass(generateAdapter = true) + data class Amount( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: BigDecimal?, + @Json(name = "maximum") + val maximum: BigDecimal?, + ) + + @JsonClass(generateAdapter = true) + data class Duration( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: Int?, + @Json(name = "maximum") + val maximum: Int?, + ) + + @JsonClass(generateAdapter = true) + data class Nft( + @Json(name = "baycId") + val baycId: Required?, + @Json(name = "maycId") + val maycId: Required?, + @Json(name = "bakcId") + val bakcId: Required?, + ) + + @JsonClass(generateAdapter = true) + data class TronResource( + @Json(name = "required") + val required: Boolean, + @Json(name = "options") + val options: List, + ) + } + } + + @JsonClass(generateAdapter = true) + data class Required( + @Json(name = "required") + val required: Boolean, + ) + } +} \ No newline at end of file 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 38e872886c..eb9e2a434c 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 @@ -10,6 +10,7 @@ import retrofit2.http.* * [REDACTED_AUTHOR] */ +@Suppress("TooManyFunctions") interface TangemTechApi { @GET("coins") @@ -76,6 +77,21 @@ interface TangemTechApi { @GET("promotion") suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse + @GET("settings/{wallet_id}") + suspend fun getUserTokensSettings( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + ): ApiResponse + + @PUT("settings/{wallet_id}") + suspend fun saveUserTokensSettings( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + @Body userTokensSettings: UserTokensSettingsResponse, + ): ApiResponse + @POST("user-network-account") suspend fun createUserNetworkAccount( @Header("card_public_key") cardPublicKey: String, @@ -83,6 +99,35 @@ interface TangemTechApi { @Body body: CreateUserNetworkAccountBody, ): ApiResponse + @POST("account") + suspend fun createUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Body body: CreateUserTokensAccountBody, + ): ApiResponse + + @PUT("account/{account_id}") + suspend fun updateUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("account_id") accountId: Int, + @Body body: UpdateUserTokensAccountBody, + ): ApiResponse + + @PUT("account/{account_id}/archive") + suspend fun archiveUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("account_id") accountId: Int, + ): ApiResponse + + @PUT("account/{account_id}/unarchive") + suspend fun restoreUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("account_id") accountId: Int, + ): ApiResponse + @GET("features") suspend fun getFeatures(): ApiResponse } \ 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 new file mode 100644 index 0000000000..80db96faa7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt @@ -0,0 +1,23 @@ +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( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + ): ApiResponse + + @PUT("user-tokens/{wallet_id}") + suspend fun saveUserTokens( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @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/TangemTechServiceApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt new file mode 100644 index 0000000000..7bd52cc1e2 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.api.tangemTech + +import com.tangem.datasource.config.models.ProviderModel +import retrofit2.http.GET + +/** + * Tangem Tech API for app services + * +[REDACTED_AUTHOR] + */ +interface TangemTechServiceApi { + + @GET("networks/providers") + suspend fun getBlockchainProviders(): Map> +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt new file mode 100644 index 0000000000..7f88539dad --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class CreateUserTokensAccountBody( + @Json(name = "accountId") + val id: Int, + @Json(name = "accountTitle") + val title: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt new file mode 100644 index 0000000000..2413b85d2b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class UpdateUserTokensAccountBody( + @Json(name = "accountTitle") + val title: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt new file mode 100644 index 0000000000..0e8341612d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class UserTokensAccountResponse( + @Json(name = "accountId") + val id: Int, + @Json(name = "accountTitle") + val title: String, + @Json(name = "archived") + val isArchived: Boolean, + @Json(name = "tokensCount") + val tokensCount: Int? = null, + @Json(name = "tokens") + val tokens: List? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt new file mode 100644 index 0000000000..9d5a0554cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class UserTokensSettingsResponse( + @Json(name = "group") + val group: UserTokensResponse.GroupType, + @Json(name = "sort") + val sort: UserTokensResponse.SortType, +) \ 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 new file mode 100644 index 0000000000..fdb8a0df7c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.tangemTech.models.v2 + +import com.squareup.moshi.Json +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +data class UserTokensResponseV2( + @Json(name = "accounts") + val accounts: List, +) { + + 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/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt deleted file mode 100644 index ce19ff204e..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.datasource.asset - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import java.io.BufferedReader -import java.io.InputStream -import javax.inject.Inject - -/** - * Implementation of asset file reader - * - * @property context application context - */ -internal class AndroidAssetReader @Inject constructor( - @ApplicationContext private val context: Context, -) : AssetReader { - - override fun readJson(fileName: String): String { - return openFile("$fileName.json") - .bufferedReader() - .use(BufferedReader::readText) - } - - override fun openFile(fileName: String): InputStream { - return context.assets - .open(fileName) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt deleted file mode 100644 index a3196c64aa..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.datasource.asset - -import java.io.InputStream - -/** - * Asset file reader - * -[REDACTED_AUTHOR] - */ -interface AssetReader { - - /** Read content of json file [fileName] from asset */ - fun readJson(fileName: String): String - - /** Open a file [file] from asset as InputStream */ - fun openFile(file: String): InputStream -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt new file mode 100644 index 0000000000..6c04362a3c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -0,0 +1,91 @@ +package com.tangem.datasource.asset.loader + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.adapter +import com.tangem.datasource.asset.reader.AssetReader +import com.tangem.datasource.di.NetworkMoshi +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Asset file loader + * + * @property assetReader asset reader + * @property moshi moshi + * + * @see Documentation + * +[REDACTED_AUTHOR] + */ +@Singleton +class AssetLoader @Inject constructor( + val assetReader: AssetReader, + @NetworkMoshi val moshi: Moshi, +) { + + /** Load content [Content] of asset file [fileName] */ + @OptIn(ExperimentalStdlibApi::class) + suspend inline fun load(fileName: String): Content? { + return runCatching { + val json = assetReader.read(fullFileName = "$fileName.json") + + moshi.adapter().fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + null + }, + ) + } + + /** Load list [V] values of asset file [fileName] */ + suspend inline fun loadList(fileName: String): List { + return runCatching { + val json = assetReader.read(fullFileName = "$fileName.json") + + val type = Types.newParameterizedType(List::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + adapter.fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + emptyList() + }, + ) + } + + /** Load map [String] keys and [V] values of asset file [fileName] */ + suspend inline fun loadMap(fileName: String): Map { + return runCatching { + val json = assetReader.read(fullFileName = "$fileName.json") + + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + adapter.fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + emptyMap() + }, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt new file mode 100644 index 0000000000..139e082aa9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.asset.reader + +import android.content.res.AssetManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.io.BufferedReader + +/** + * Implementation of asset file reader + * + * @property assetManager asset manager + * @property dispatchers dispatchers + */ +internal class AndroidAssetReader( + private val assetManager: AssetManager, + private val dispatchers: CoroutineDispatcherProvider, +) : AssetReader { + + override suspend fun read(fullFileName: String): String = withContext(dispatchers.io) { + assetManager.open(fullFileName).bufferedReader() + .use(BufferedReader::readText) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt new file mode 100644 index 0000000000..a8c2053a87 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.asset.reader + +/** + * Asset file reader + * + * @see Documentation + * +[REDACTED_AUTHOR] + */ +interface AssetReader { + + /** + * Read content of file from assets + * + * @param fullFileName name of file with extension. Example: file.json + */ + suspend fun read(fullFileName: String): String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManager.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManager.kt index 5cfaed8562..9c8a0d8127 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManager.kt @@ -10,7 +10,7 @@ interface ConfigManager { val config: Config - fun load(configLoader: Loader, onComplete: ((config: Config) -> Unit)? = null) + suspend fun load(configLoader: Loader, onComplete: ((config: Config) -> Unit)? = null) fun turnOff(name: String) diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index d639cfbe6a..6bda8dd093 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -19,7 +19,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { private var defaultConfig = Config() - override fun load(configLoader: Loader, onComplete: ((config: Config) -> Unit)?) { + override suspend fun load(configLoader: Loader, onComplete: ((config: Config) -> Unit)?) { configLoader.load { configModel -> setupFeature(configModel.features) setupConfigValues(configModel.configValues) @@ -99,13 +99,17 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { ), chiaFireAcademyApiKey = configValues.chiaFireAcademyApiKey, chiaTangemApiKey = configValues.chiaTangemApiKey, + hederaArkhiaApiKey = configValues.hederaArkhiaKey, + polygonScanApiKey = configValues.polygonScanApiKey, + bittensorDwellirApiKey = configValues.bittensorDwellirApiKey, + bittensorOnfinalityApiKey = configValues.bittensorOnfinalityKey, ), - appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, sprinklr = configValues.sprinklr, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, express = if (BuildConfig.ENVIRONMENT == "dev") configValues.devExpress else configValues.express, + stakeKitApiKey = configValues.stakeKitApiKey, ) } @@ -146,8 +150,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { blockBookRest = accessTokens.bitcoin?.blockBookRest, ), algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), - zkSync = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC), - polygonZkevm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC), + zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC), + polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC), base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt index 829b539552..205537191a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt @@ -1,38 +1,25 @@ package com.tangem.datasource.config -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.models.ConfigModel import com.tangem.datasource.config.models.ConfigValueModel import com.tangem.datasource.config.models.FeatureModel -import timber.log.Timber /** [REDACTED_AUTHOR] */ class FeaturesLocalLoader( - private val assetReader: AssetReader, - private val moshi: Moshi, + private val assetLoader: AssetLoader, buildEnvironment: String, ) : Loader { private val featuresName = "features_$buildEnvironment" private val configValuesName = "tangem-app-config/config_$buildEnvironment" - override fun load(onComplete: (ConfigModel) -> Unit) { - val config = try { - val featureAdapter: JsonAdapter = moshi.adapter(FeatureModel::class.java) - val valuesAdapter: JsonAdapter = moshi.adapter(ConfigValueModel::class.java) - - val jsonFeatures = assetReader.readJson(featuresName) - val jsonConfigValues = assetReader.readJson(configValuesName) - - ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues)) - } catch (ex: Exception) { - Timber.e(ex) - ConfigModel.empty() - } - onComplete(config) + override suspend fun load(onComplete: (ConfigModel) -> Unit) { + ConfigModel( + features = assetLoader.load(fileName = featuresName), + configValues = assetLoader.load(fileName = configValuesName), + ).also(onComplete) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/Loader.kt b/core/datasource/src/main/java/com/tangem/datasource/config/Loader.kt index 515e2b0d23..c5f9b0073e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/Loader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/Loader.kt @@ -4,5 +4,5 @@ package com.tangem.datasource.config [REDACTED_AUTHOR] */ interface Loader { - fun load(onComplete: (T) -> Unit) + suspend fun load(onComplete: (T) -> Unit) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 25e8595d33..3f54641502 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -8,7 +8,6 @@ data class Config( val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C", val mercuryoWidgetId: String = "", val mercuryoSecret: String = "", - val appsFlyerDevKey: String = "", val amplitudeApiKey: String = "", val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), val isTopUpEnabled: Boolean = false, @@ -18,4 +17,5 @@ data class Config( val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, val express: ExpressModel? = null, + val stakeKitApiKey: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 94987bf391..685b4e3ffe 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass [REDACTED_AUTHOR] */ +// TODO remove class FeatureModel( val isTopUpEnabled: Boolean, val isCreatingTwinCardsAllowed: Boolean, @@ -30,7 +31,6 @@ class ConfigValueModel( @Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys, val blockcypherTokens: Set?, val infuraProjectId: String?, - val appsFlyer: AppsFlyer, val sprinklr: SprinklrConfig?, val tronGridApiKey: String, val amplitudeApiKey: String, @@ -41,6 +41,11 @@ class ConfigValueModel( val chiaTangemApiKey: String?, val devExpress: ExpressModel?, val express: ExpressModel?, + @Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?, + val polygonScanApiKey: String?, + val stakeKitApiKey: String?, + @Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?, + @Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?, ) @JsonClass(generateAdapter = true) @@ -80,11 +85,6 @@ data class GetBlockToken( @Json(name = "rosetta") val rosetta: String?, ) -data class AppsFlyer( - val appsFlyerDevKey: String, - val appsFlyerAppID: String, -) - class ConfigModel( val features: FeatureModel?, val configValues: ConfigValueModel?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ProviderModel.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ProviderModel.kt new file mode 100644 index 0000000000..6bd7f5c07a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/ProviderModel.kt @@ -0,0 +1,32 @@ +package com.tangem.datasource.config.models + +import com.squareup.moshi.Json + +/** Config provider model */ +sealed class ProviderModel { + + /** + * Example, + * { + * "type": "public", + * "url": "https://example.com" + * } + */ + data class Public( + @Json(name = "url") val url: String, + ) : ProviderModel() + + /** + * Example, + * { + * "type": "private", + * "name": "nownodes" + * } + */ + data class Private( + @Json(name = "name") val name: String, + ) : ProviderModel() + + /** Unsupported type */ + data object UnsupportedType : ProviderModel() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AccountCreatorModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AccountCreatorModule.kt deleted file mode 100644 index 3b97ba24d9..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AccountCreatorModule.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.blockchain.common.AccountCreator -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.blockchain.DefaultAccountCreator -import com.tangem.lib.auth.AuthProvider -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 AccountCreatorModule { - - @Provides - @Singleton - fun provideAccountCreator(authProvider: AuthProvider, tangemTechApi: TangemTechApi): AccountCreator { - return DefaultAccountCreator(authProvider, tangemTechApi) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt deleted file mode 100644 index 7df5502310..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.asset.AndroidAssetReader -import com.tangem.datasource.asset.AssetReader -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 AssetModule { - - @Binds - @Singleton - fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt new file mode 100644 index 0000000000..196ec8d836 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetReaderModule.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.di + +import android.content.Context +import com.tangem.datasource.asset.reader.AndroidAssetReader +import com.tangem.datasource.asset.reader.AssetReader +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 javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AssetReaderModule { + + @Singleton + @Provides + fun providesAsserReader( + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): AssetReader { + return AndroidAssetReader(context.assets, dispatchers) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/BlockchainDataStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/BlockchainDataStorageModule.kt deleted file mode 100644 index 3d285b3fbe..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/BlockchainDataStorageModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage -import com.tangem.datasource.local.preferences.AppPreferencesStore -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 BlockchainDataStorageModule { - - @Provides - @Singleton - fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage { - return DefaultBlockchainDataStorage(appPreferencesStore) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index c22d4433bf..ddd3386e58 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -1,11 +1,15 @@ package com.tangem.datasource.di import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter -import com.tangem.datasource.api.common.BigDecimalAdapter -import com.tangem.datasource.api.common.DateTimeAdapter -import com.tangem.datasource.api.common.LocalDateAdapter +import com.tangem.datasource.api.common.adapter.BigDecimalAdapter +import com.tangem.datasource.api.common.adapter.DateTimeAdapter +import com.tangem.datasource.api.common.adapter.LocalDateAdapter +import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter +import com.tangem.datasource.api.stakekit.models.response.model.Token +import com.tangem.datasource.config.models.ProviderModel import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,10 +25,20 @@ class MoshiModule { @NetworkMoshi fun provideNetworkMoshi(): Moshi { return Moshi.Builder() + .add( + PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type") + .withSubtype(ProviderModel.Public::class.java, "public") + .withSubtype(ProviderModel.Private::class.java, "private") + .withDefaultValue(ProviderModel.UnsupportedType), + ) .add(BigDecimalAdapter()) .add(LocalDateAdapter()) .add(DateTimeAdapter()) .add(KotlinJsonAdapterFactory()) + .add( + Token.NetworkType::class.java, + UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN), + ) .build() } 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 e52008bbc5..ed7b6b5621 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 @@ -5,12 +5,17 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi +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.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.utils.RequestHeader import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers -import com.tangem.lib.auth.AppVersionProvider import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,6 +24,7 @@ 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 @@ -34,7 +40,7 @@ class NetworkModule { appVersionProvider: AppVersionProvider, ): TangemExpressApi { val url = if (BuildConfig.ENVIRONMENT == "dev") { - DEV_EXPRESS_BASE_URL + STAGE_EXPRESS_BASE_URL } else { PROD_EXPRESS_BASE_URL } @@ -55,55 +61,117 @@ class NetworkModule { @Provides @Singleton - fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { + fun provideStakeKitApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + stakeKitAuthProvider: StakeKitAuthProvider, + ): StakeKitApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(PROD_TANGEM_TECH_BASE_URL) + .baseUrl(STAKEKIT_BASE_URL) .client( OkHttpClient.Builder() - .addHeaders( - CacheControlHeader, - // TODO("refactor header init") get auth data after biometric auth to avoid race condition - // AuthenticationHeader(authProvider), - ) + .addHeaders(StakeKit(stakeKitAuthProvider)) .addLoggers(context) .build(), ) .build() - .create(TangemTechApi::class.java) + .create(StakeKitApi::class.java) + } + + @Provides + @Singleton + fun provideTangemTechApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechApi { + return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V1_TANGEM_TECH_BASE_URL) + } + + @Provides + @Singleton + fun provideTangemTechApiV2( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechApiV2 { + return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V2_TANGEM_TECH_BASE_URL) } @Provides @DevTangemApi @Singleton - fun provideTangemTechDevApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { + fun provideTangemTechDevApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechApi { + return provideTangemTechApiInternal(moshi, context, appVersionProvider, DEV_V1_TANGEM_TECH_BASE_URL) + } + + @Provides + @Singleton + fun provideTangemTechServiceApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechServiceApi { + return provideTangemTechApiInternal( + moshi = moshi, + context = context, + appVersionProvider = appVersionProvider, + baseUrl = PROD_V1_TANGEM_TECH_BASE_URL, + timeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, + requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)), + ) + } + + private inline fun provideTangemTechApiInternal( + moshi: Moshi, + context: Context, + appVersionProvider: AppVersionProvider, + baseUrl: String, + timeoutSeconds: Long? = null, + requestHeaders: List = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)), + ): T { + val client = OkHttpClient.Builder() + .let { builder -> + if (timeoutSeconds != null) { + builder.callTimeout(timeoutSeconds, TimeUnit.SECONDS) + } else { + builder + } + } + .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()) - .baseUrl(DEV_TANGEM_TECH_BASE_URL) - .client( - OkHttpClient.Builder() - .addHeaders( - CacheControlHeader, - // TODO("refactor header init") get auth data after biometric auth to avoid race condition - // AuthenticationHeader(authProvider), - ) - .addLoggers(context) - .build(), - ) + .baseUrl(baseUrl) + .client(client) .build() - .create(TangemTechApi::class.java) + .create(T::class.java) } private companion object { + const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/" const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/" + const val STAGE_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" - const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" - const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" + const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" - const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/" - const val API_ONE_INCH_TIMEOUT_MS = 5000L + const val PROD_V1_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" + const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" + + const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt index 229722e50d..e23f80cbb6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt @@ -1,8 +1,6 @@ package com.tangem.datasource.di -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage import com.tangem.datasource.local.testnet.TestnetTokensStorage import dagger.Module @@ -16,12 +14,11 @@ import javax.inject.Singleton */ @Module @InstallIn(SingletonComponent::class) -object TestnetTokensStorageModule { +internal object TestnetTokensStorageModule { - @OptIn(ExperimentalStdlibApi::class) @Provides @Singleton - fun providesTestnetTokensStorage(assetReader: AssetReader, @SdkMoshi moshi: Moshi): TestnetTokensStorage { - return DefaultTestnetTokensStorage(assetReader = assetReader, adapter = moshi.adapter()) + fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage { + return DefaultTestnetTokensStorage(assetLoader) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/TangemDatabase.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/TangemDatabase.kt new file mode 100644 index 0000000000..7961908686 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/TangemDatabase.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.tangem.datasource.local.db.dao.CryptoCurrenciesAccountDao +import com.tangem.datasource.local.db.dao.CryptoCurrencyDao +import com.tangem.datasource.local.db.dao.UserWalletDao +import com.tangem.datasource.local.db.entity.CryptoCurrenciesAccountEntity +import com.tangem.datasource.local.db.entity.CryptoCurrencyEntity +import com.tangem.datasource.local.db.entity.UserWalletEntity +import com.tangem.datasource.local.db.utils.Converters + +@Database( + entities = [ + UserWalletEntity::class, + CryptoCurrenciesAccountEntity::class, + CryptoCurrencyEntity::class, + ], + version = 1, +) +@TypeConverters(Converters::class) +abstract class TangemDatabase : RoomDatabase() { + + abstract fun userWalletDao(): UserWalletDao + + abstract fun cryptoCurrencyDao(): CryptoCurrencyDao + + abstract fun cryptoCurrenciesAccountDao(): CryptoCurrenciesAccountDao +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrenciesAccountDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrenciesAccountDao.kt new file mode 100644 index 0000000000..92cbe0b2fc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrenciesAccountDao.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.tangem.datasource.local.db.entity.CryptoCurrenciesAccountEntity +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +@Dao +interface CryptoCurrenciesAccountDao { + + @Insert + suspend fun insert(account: CryptoCurrenciesAccountEntity) + + @Update + suspend fun update(account: CryptoCurrenciesAccountEntity) + + @Query("SELECT * FROM CryptoCurrenciesAccountEntity WHERE userWalletId = :userWalletId") + fun observeByUserWalletId(userWalletId: UserWalletId): Flow> + + @Query("SELECT * FROM CryptoCurrenciesAccountEntity WHERE userWalletId = :userWalletId") + suspend fun selectByUserWalletId(userWalletId: UserWalletId): List +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrencyDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrencyDao.kt new file mode 100644 index 0000000000..a7cbfec3cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrencyDao.kt @@ -0,0 +1,39 @@ +package com.tangem.datasource.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.tangem.datasource.local.db.entity.CryptoCurrencyEntity +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +@Dao +interface CryptoCurrencyDao { + + @Insert + suspend fun insert(currencies: List) + + @Query( + """ + SELECT * FROM CryptoCurrencyEntity + WHERE userWalletId = :userWalletId AND accountId = :accountId + """, + ) + suspend fun selectByAccountId(userWalletId: UserWalletId, accountId: Int): List + + @Query( + """ + SELECT * FROM CryptoCurrencyEntity + WHERE userWalletId = :userWalletId AND accountId = :accountId + """, + ) + fun observeByAccountId(userWalletId: UserWalletId, accountId: Int): Flow> + + @Query( + """ + SELECT COUNT(*) FROM CryptoCurrencyEntity + WHERE userWalletId = :userWalletId + """, + ) + suspend fun countByUserWalletId(userWalletId: UserWalletId): Int +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/UserWalletDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/UserWalletDao.kt new file mode 100644 index 0000000000..31bca725e8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/UserWalletDao.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.tangem.datasource.local.db.entity.UserWalletEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface UserWalletDao { + + @Insert + suspend fun insert(vararg wallets: UserWalletEntity) + + @Update + suspend fun update(wallet: UserWalletEntity) + + @Query("SELECT * FROM UserWalletEntity") + suspend fun selectAll(): List + + @Query("SELECT * FROM UserWalletEntity") + fun observeAll(): Flow> + + @Query("SELECT * FROM UserWalletEntity WHERE id = :id") + suspend fun selectById(id: String): UserWalletEntity? + + @Query("SELECT * FROM UserWalletEntity WHERE id = :id") + fun observeById(id: String): Flow +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/di/TangemDatabaseModule.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/di/TangemDatabaseModule.kt new file mode 100644 index 0000000000..9b52c19cb5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/di/TangemDatabaseModule.kt @@ -0,0 +1,28 @@ +package com.tangem.datasource.local.db.di + +import android.content.Context +import androidx.room.Room +import com.tangem.datasource.local.db.TangemDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemDatabaseModule { + + private const val DATABASE_NAME = "tangem_database.db" + + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): TangemDatabase { + return Room.databaseBuilder( + context, + TangemDatabase::class.java, + DATABASE_NAME, + ).build() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrenciesAccountEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrenciesAccountEntity.kt new file mode 100644 index 0000000000..d9c940359a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrenciesAccountEntity.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.db.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = UserWalletEntity::class, + parentColumns = ["id"], + childColumns = ["userWalletId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(value = ["id"]), + Index(value = ["userWalletId"]), + ], +) +data class CryptoCurrenciesAccountEntity( + @PrimaryKey + val id: Int, + val userWalletId: String, + val title: String, + val currenciesCount: Int, + val isArchived: Boolean, + val ordinalNumber: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrencyEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrencyEntity.kt new file mode 100644 index 0000000000..5aab1dcf2f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrencyEntity.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.local.db.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CryptoCurrenciesAccountEntity::class, + parentColumns = ["id"], + childColumns = ["accountId"], + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = UserWalletEntity::class, + parentColumns = ["id"], + childColumns = ["userWalletId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(value = ["currencyBackendId"]), + Index(value = ["networkId"]), + Index(value = ["accountId"]), + Index(value = ["userWalletId"]), + ], +) +data class CryptoCurrencyEntity( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val currencyBackendId: String?, + val networkId: String, + val accountId: Int, + val userWalletId: String, + val name: String, + val symbol: String, + val decimals: Int, + val contractAddress: String?, + val derivationPath: String?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/UserWalletEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/UserWalletEntity.kt new file mode 100644 index 0000000000..9f07d4a325 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/UserWalletEntity.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.local.db.entity + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + indices = [ + Index(value = ["id"], unique = true), + Index(value = ["ordinalNumber"], unique = true), + ], +) +data class UserWalletEntity( + @PrimaryKey + val id: String, + val name: String, + val artworkUrl: String, + val isMultiCurrency: Boolean, + val hasBackupError: Boolean, + val cardsInWallet: Set, + val ordinalNumber: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/utils/Converters.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/utils/Converters.kt new file mode 100644 index 0000000000..47c939a20e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/utils/Converters.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.local.db.utils + +import androidx.room.TypeConverter +import com.tangem.domain.wallets.models.UserWalletId + +internal class Converters { + + @TypeConverter + fun setFromString(value: String): Set { + return value.split(",").toSet() + } + + @TypeConverter + fun setToString(set: Set): String { + return set.joinToString(",") + } + + @TypeConverter + fun userWalletIdFromString(value: String): UserWalletId { + return UserWalletId(value) + } + + @TypeConverter + fun userWalletIdToString(userWalletId: UserWalletId): String { + return userWalletId.stringValue + } +} \ 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 8a7c3c3dff..e069422752 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 @@ -5,9 +5,13 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_ import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_APPLICATION_STOPPED_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN /** @@ -19,6 +23,8 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } + val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") } @@ -79,6 +85,14 @@ object PreferencesKeys { val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") } + val WAS_APPLICATION_STOPPED_KEY by lazy { booleanPreferencesKey(name = "applicationStopped") } + + val SHOULD_OPEN_WELCOME_ON_RESUME_KEY by lazy { booleanPreferencesKey(name = "openWelcomeOnResume") } + + val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") } + + val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } + fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") } @@ -86,6 +100,7 @@ object PreferencesKeys { internal fun getTapPrefKeysToMigrate(): Set { return setOf( SAVE_USER_WALLETS_KEY, + SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, APP_LAUNCH_COUNT_KEY, SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY, FUNDS_FOUND_DATE_KEY, @@ -93,6 +108,9 @@ internal fun getTapPrefKeysToMigrate(): Set { USED_CARDS_INFO_KEY, WAS_TWINS_ONBOARDING_SHOWN, IS_TANGEM_TOS_ACCEPTED_KEY, + WAS_APPLICATION_STOPPED_KEY, + SHOULD_OPEN_WELCOME_ON_RESUME_KEY, + SHOULD_SAVE_ACCESS_CODES_KEY, ) .map(Preferences.Key<*>::name) .toSet() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/testnet/DefaultTestnetTokensStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/testnet/DefaultTestnetTokensStorage.kt index d4a3a066e1..b88bdda0bc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/testnet/DefaultTestnetTokensStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/testnet/DefaultTestnetTokensStorage.kt @@ -1,28 +1,21 @@ package com.tangem.datasource.local.testnet -import com.squareup.moshi.JsonAdapter -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.testnet.models.TestnetTokensConfig /** * Default implementation for storing testnet tokens data * - * @property assetReader file reader from assets - * @property adapter adapter for parsing testnet tokens config + * @property assetLoader asset loader * [REDACTED_AUTHOR] */ internal class DefaultTestnetTokensStorage( - private val assetReader: AssetReader, - private val adapter: JsonAdapter, + private val assetLoader: AssetLoader, ) : TestnetTokensStorage { - override fun getConfig(): TestnetTokensConfig { - return requireNotNull( - value = adapter.fromJson( - assetReader.readJson(fileName = LOCAL_CONFIG_PATH), - ), - ) + override suspend fun getConfig(): TestnetTokensConfig { + return requireNotNull(assetLoader.load(fileName = LOCAL_CONFIG_PATH)) } private companion object { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/testnet/TestnetTokensStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/testnet/TestnetTokensStorage.kt index 7f091e98e6..7cbd7a6602 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/testnet/TestnetTokensStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/testnet/TestnetTokensStorage.kt @@ -10,5 +10,5 @@ import com.tangem.datasource.local.testnet.models.TestnetTokensConfig interface TestnetTokensStorage { /** Get a testnet tokens data */ - fun getConfig(): TestnetTokensConfig + suspend fun getConfig(): TestnetTokensConfig } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index bd725ea0b6..c9fa81608f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -1,9 +1,9 @@ package com.tangem.datasource.utils -import com.tangem.lib.auth.AppVersionProvider -import com.tangem.lib.auth.AuthBearerProvider -import com.tangem.lib.auth.AuthProvider +import com.tangem.datasource.api.common.AuthProvider import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.StakeKitAuthProvider +import com.tangem.utils.version.AppVersionProvider /** * Presentation of request header @@ -15,7 +15,7 @@ sealed class RequestHeader(vararg pairs: Pair String>) { /** Header list */ val values: List String>> = pairs.toList() - object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" }) + data object CacheControlHeader : RequestHeader("Cache-Control" to { "max-age=600" }) class AuthenticationHeader(authProvider: AuthProvider) : RequestHeader( "card_id" to { authProvider.getCardId() }, @@ -28,12 +28,12 @@ sealed class RequestHeader(vararg pairs: Pair String>) { "session-id" to { expressAuthProvider.getSessionId() }, ) - class AuthBearerHeader(authBearerProvider: AuthBearerProvider) : RequestHeader( - "Authorization" to { "Bearer " + authBearerProvider.getApiKey() }, - ) - class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader( - "version" to { appVersionProvider.getAppVersion() }, + "version" to { appVersionProvider.versionName }, "platform" to { "android" }, ) + + class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader( + "X-API-KEY" to { stakeKitAuthProvider.getApiKey() }, + ) } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt new file mode 100644 index 0000000000..529f1271a9 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/loader/AssetLoaderTest.kt @@ -0,0 +1,119 @@ +package com.tangem.datasource.asset.loader + +import com.google.common.truth.Truth +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.adapter +import com.tangem.datasource.api.express.models.response.Asset +import com.tangem.datasource.asset.reader.AssetReader +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalStdlibApi::class) +class AssetLoaderTest { + + private val assetReader = mockk() + private val moshi = mockk() + private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi) + + @Test + fun load() = runTest { + everyReadingJson() returns assetObjectJson + + val jsonAdapter = mockk>() + every { moshi.adapter() } returns jsonAdapter + every { jsonAdapter.fromJson(assetObjectJson) } returns assetObject + + val actual = assetLoader.load(fileName = JSON_FILE_NAME) + + coVerifyOrder { + assetReader.read("$JSON_FILE_NAME.json") + jsonAdapter.fromJson(assetObjectJson) + } + + Truth.assertThat(actual).isEqualTo(assetObject) + } + + @Test + fun loadList() = runTest { + everyReadingJson() returns assetListJson + + val jsonAdapter = mockk>>() + val types = Types.newParameterizedType(List::class.java, Asset::class.java) + every { moshi.adapter>(types) } returns jsonAdapter + every { jsonAdapter.fromJson(assetListJson) } returns assetList + + val actual = assetLoader.loadList(fileName = JSON_FILE_NAME) + + coVerifyOrder { + assetReader.read("$JSON_FILE_NAME.json") + jsonAdapter.fromJson(assetListJson) + } + + Truth.assertThat(actual).isEqualTo(assetList) + } + + @Test + fun loadMap() = runTest { + everyReadingJson() returns assetMapJson + + val jsonAdapter = mockk>>() + val types = Types.newParameterizedType(Map::class.java, String::class.java, Asset::class.java) + every { moshi.adapter>(types) } returns jsonAdapter + every { jsonAdapter.fromJson(assetMapJson) } returns assetMap + + val actual = assetLoader.loadMap(fileName = JSON_FILE_NAME) + + coVerifyOrder { + assetReader.read("$JSON_FILE_NAME.json") + jsonAdapter.fromJson(assetMapJson) + } + + Truth.assertThat(actual).isEqualTo(assetMap) + } + + private fun everyReadingJson() = coEvery { assetReader.read("$JSON_FILE_NAME.json") } + + private companion object { + + const val JSON_FILE_NAME = "config" + + val assetObject = Asset( + contractAddress = "0x0000000000000000000000000000000000000000", + network = "Network", + exchangeAvailable = true, + ) + + val assetList = listOf(assetObject, assetObject) + + val assetMap = mapOf("key1" to assetObject, "key2" to assetObject) + + val assetObjectJson = """ + { + "contractAddress": "${assetObject.contractAddress}", + "network": "${assetObject.network}", + "exchangeAvailable": ${assetObject.exchangeAvailable} + } + """.trimIndent() + + val assetListJson = """ + $assetObjectJson, + $assetObjectJson + """.trimIndent() + + val assetMapJson = """ + { + "key1": $assetObjectJson, + "key2": $assetObjectJson + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt new file mode 100644 index 0000000000..c1beef670e --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/asset/reader/AndroidAssetReaderTest.kt @@ -0,0 +1,51 @@ +package com.tangem.datasource.asset.reader + +import android.content.res.AssetManager +import com.google.common.truth.Truth +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.io.IOException + +/** +[REDACTED_AUTHOR] + */ +internal class AndroidAssetReaderTest { + + private val assetManager = mockk() + private val assetReader = AndroidAssetReader(assetManager, TestingCoroutineDispatcherProvider()) + + @Test + fun read_content() = runTest { + every { assetManager.open(FILE_NAME) } returns json.byteInputStream() + + val actual = assetReader.read(fullFileName = FILE_NAME) + + Truth.assertThat(actual).isEqualTo(json) + } + + @Test + fun read_error() = runTest { + val exception = IOException("Error") + every { assetManager.open(FILE_NAME) } throws exception + + runCatching { assetReader.read(fullFileName = FILE_NAME) } + .onSuccess { throw IllegalStateException("Error should be thrown") } + .onFailure { + Truth.assertThat(it).isEqualTo(exception) + } + } + + private companion object { + + const val FILE_NAME = "file.json" + + val json = """ + { + "key": "value" + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/data/source/preferences/.gitignore b/core/decompose/.gitignore similarity index 100% rename from data/source/preferences/.gitignore rename to core/decompose/.gitignore diff --git a/core/decompose/build.gradle.kts b/core/decompose/build.gradle.kts new file mode 100644 index 0000000000..aa1b6e3c82 --- /dev/null +++ b/core/decompose/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +dependencies { + api(projects.core.utils) + + api(deps.decompose) + implementation(deps.kotlin.coroutines) + + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt new file mode 100644 index 0000000000..9426735800 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContext.kt @@ -0,0 +1,23 @@ +package com.tangem.core.decompose.context + +import com.arkivanov.decompose.ComponentContext +import com.tangem.core.decompose.di.HiltComponentBuilderOwner +import com.tangem.core.decompose.navigation.NavigationOwner +import com.tangem.core.decompose.ui.UiMessageSenderOwner +import com.tangem.core.decompose.utils.ComponentScopeOwner +import com.tangem.core.decompose.utils.DispatchersOwner +import com.tangem.core.decompose.utils.TagsOwner + +/** + * Interface for the application component context. + * + * It combines several other interfaces related to navigation, dispatching, UI messaging, etc. + */ +interface AppComponentContext : + ComponentContext, + NavigationOwner, + ComponentScopeOwner, + DispatchersOwner, + UiMessageSenderOwner, + HiltComponentBuilderOwner, + TagsOwner \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt new file mode 100644 index 0000000000..146a7e2bb0 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/AppComponentContextChildrenFactory.kt @@ -0,0 +1,73 @@ +package com.tangem.core.decompose.context + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.childContext +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.tangem.core.decompose.di.HiltComponentBuilderOwner +import com.tangem.core.decompose.navigation.NavigationOwner +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.DefaultUiMessageSender +import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.decompose.ui.UiMessageSenderOwner +import com.tangem.core.decompose.utils.ComponentCoroutineScope +import com.tangem.core.decompose.utils.DispatchersOwner +import kotlinx.coroutines.CoroutineScope + +/** + * Creates a new child [AppComponentContext] with the provided [key] and optional [lifecycle]. + * + * @param key The key to use. + * @param lifecycle The [Lifecycle] to use. If not provided, the parent's lifecycle will be used. + * @param router The [Router] to use in the child. If not provided, the parent's router will be used. + * @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will + * be used. + + * + * @see childByContext + * */ +fun AppComponentContext.child( + key: String, + lifecycle: Lifecycle? = null, + router: Router? = null, + messageHandler: UiMessageHandler? = null, +): AppComponentContext = childByContext( + componentContext = childContext(key, lifecycle), + router = router, + messageHandler = messageHandler, +) + +/** + * Creates a new child [AppComponentContext] with the provided [componentContext]. + * + * @param componentContext The [ComponentContext] to use. + * @param router The [Router] to use in the child. If not provided, the parent's router will be used. + * @param messageHandler The [UiMessageHandler] to use in the child. If not provided, the parent's message sender will + * be used. + + * + * @see child + * */ +fun AppComponentContext.childByContext( + componentContext: ComponentContext, + router: Router? = null, + messageHandler: UiMessageHandler? = null, +): AppComponentContext = object : + AppComponentContext, + ComponentContext by componentContext, + NavigationOwner by this@childByContext, + UiMessageSenderOwner by this@childByContext, + DispatchersOwner by this@childByContext, + HiltComponentBuilderOwner by this@childByContext { + + override val tags: HashMap = HashMap() + + override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers) + + override val messageSender: UiMessageSender = messageHandler + ?.let(::DefaultUiMessageSender) + ?: this@childByContext.messageSender + + override val router: Router + get() = router ?: this@childByContext.router +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt new file mode 100644 index 0000000000..586779ec4a --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/context/DefaultAppComponentContext.kt @@ -0,0 +1,35 @@ +package com.tangem.core.decompose.context + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.navigation.AppNavigationProvider +import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider +import com.tangem.core.decompose.navigation.DefaultRouter +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.DefaultUiMessageSender +import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.decompose.utils.ComponentCoroutineScope +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope + +class DefaultAppComponentContext( + componentContext: ComponentContext, + messageHandler: UiMessageHandler, + override val dispatchers: CoroutineDispatcherProvider, + override val hiltComponentBuilder: DecomposeComponent.Builder, +) : AppComponentContext, ComponentContext by componentContext { + + override val tags: HashMap = HashMap() + + override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers) + + override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler) + + override val navigationProvider: AppNavigationProvider + get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() } + + override val router: Router + get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/ComponentScoped.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/ComponentScoped.kt new file mode 100644 index 0000000000..b5fde3b58f --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/ComponentScoped.kt @@ -0,0 +1,9 @@ +package com.tangem.core.decompose.di + +/** + * Annotation for marking a dependency as a component scoped. + * + * This means that the lifecycle of the dependency is limited to the lifecycle of the component it is attached to. + */ +@Retention(AnnotationRetention.SOURCE) +annotation class ComponentScoped \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt new file mode 100644 index 0000000000..b5df451866 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt @@ -0,0 +1,47 @@ +package com.tangem.core.decompose.di + +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import dagger.BindsInstance +import dagger.hilt.DefineComponent +import dagger.hilt.components.SingletonComponent + +/** + * Interface for the Decompose component. + * + * It is annotated as [ComponentScoped], meaning it has a lifecycle that is scoped to the component. + */ +@ComponentScoped +@DefineComponent(parent = SingletonComponent::class) +interface DecomposeComponent { + + /** + * Builder interface for the component. + */ + @DefineComponent.Builder + interface Builder { + + /** + * Sets the router for the component. + * + * @param router The router to set. + * @return The builder instance. + */ + fun router(@BindsInstance router: Router): Builder + + /** + * Sets the UI message sender for the component. + * + * @param uiMessageSender The UI message sender to set. + * @return The builder instance. + */ + fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder + + /** + * Builds the Decompose component. + * + * @return The built Decompose component. + */ + fun build(): DecomposeComponent + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/HiltComponentBuilderOwner.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/HiltComponentBuilderOwner.kt new file mode 100644 index 0000000000..d210c58019 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/HiltComponentBuilderOwner.kt @@ -0,0 +1,12 @@ +package com.tangem.core.decompose.di + +/** + * Interface for owning a Hilt component builder. + */ +interface HiltComponentBuilderOwner { + + /** + * Provides access to the Hilt component builder instance. + */ + val hiltComponentBuilder: DecomposeComponent.Builder +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt new file mode 100644 index 0000000000..b9e3987632 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/RootAppComponentContext.kt @@ -0,0 +1,6 @@ +package com.tangem.core.decompose.di + +import javax.inject.Qualifier + +@Qualifier +annotation class RootAppComponentContext \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt new file mode 100644 index 0000000000..c845658265 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt @@ -0,0 +1,78 @@ +package com.tangem.core.decompose.model + +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +/** + * Abstract class for a component's model. + * + * It provides access to the coroutine dispatchers and a coroutine scope which will survive re-creation of component + * and will be destroyed when the component is destroyed. + * + * Also, it can inject and use some component features like [Router] and [UiMessageSender]. + */ +abstract class Model : InstanceKeeper.Instance { + + /** + * Provides access to the coroutine dispatchers. + */ + protected abstract val dispatchers: CoroutineDispatcherProvider + + /** + * The coroutine scope for the model. That will be cancelled when the model is destroyed. + */ + protected val modelScope by lazy { + CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob()) + } + + override fun onDestroy() { + runCatching { modelScope.cancel() } + } + + /** + * Launches a coroutine in the model's scope and updates the [progressFlow] with the progress state. + * + * @param progressFlow The [SharedFlow] to emit the progress state. + * @param dispatcher The [CoroutineDispatcher] to launch the coroutine. Default is [Dispatchers.Main.immediate]. + * @param block The block of code to execute. + * + * @return The [Job] of the launched coroutine. + * */ + protected inline fun withProgress( + progressFlow: MutableSharedFlow, + dispatcher: CoroutineDispatcher = dispatchers.mainImmediate, + crossinline block: suspend () -> Unit, + ): Job = modelScope.launch(dispatcher) { + progressFlow.emit(value = true) + + try { + block() + } finally { + withContext(NonCancellable) { + progressFlow.emit(value = false) + } + } + } + + /** + * Converts a cold [Flow] to a hot [SharedFlow] that will be shared in the model's scope. + * + * @param started The [SharingStarted] strategy to start sharing the flow. Default is [SharingStarted.WhileSubscribed]. + * @param replay The number of values to replay. Default is `1`. + * + * @return The [SharedFlow] that will be shared in the model's scope. + * @see [Flow.shareIn] + * */ + protected fun Flow.share( + started: SharingStarted = SharingStarted.WhileSubscribed(), + replay: Int = 1, + ): SharedFlow = shareIn( + scope = modelScope, + started = started, + replay = replay, + ) +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt new file mode 100644 index 0000000000..58cad4cf7a --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt @@ -0,0 +1,51 @@ +package com.tangem.core.decompose.model + +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.di.DecomposeComponent +import dagger.hilt.EntryPoint +import dagger.hilt.EntryPoints +import dagger.hilt.InstallIn +import javax.inject.Provider + +/** + * Entry point for the models in the application. + * + * It provides a map of model providers. + */ +@EntryPoint +@InstallIn(DecomposeComponent::class) +interface ModelsEntryPoint { + + fun models(): Map, Provider> +} + +/** + * Gets or creates a component's [Model]. + */ +inline fun AppComponentContext.getOrCreateModel(): M { + val modelKey = "model_${M::class.simpleName}" + + val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") { + val hiltComponent = hiltComponentBuilder + .router(router) + .uiMessageSender(messageSender) + .build() + + EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java) + } + + val model = instanceKeeper.getOrCreate(modelKey) { + requireNotNull(entryPoint.models()[M::class.java]?.get()) { + "Model ${M::class.simpleName} is not provided" + } + } + + val isModelExist = tags.getOrElse(modelKey) { false } as Boolean + if (!isModelExist) { + tags[modelKey] = true + } + + return model as M +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/AppNavigationProvider.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/AppNavigationProvider.kt new file mode 100644 index 0000000000..a269e5622f --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/AppNavigationProvider.kt @@ -0,0 +1,28 @@ +@file:Suppress("UNCHECKED_CAST") + +package com.tangem.core.decompose.navigation + +import com.arkivanov.decompose.router.stack.StackNavigation + +/** + * Interface for providing application navigation. + * It provides or creates a StackNavigation instance for the application. + */ +interface AppNavigationProvider { + + /** + * Gets or creates a StackNavigation instance. + * + * @return The StackNavigation instance. + */ + fun getOrCreate(): StackNavigation +} + +/** + * Gets or creates a [StackNavigation] instance of a specific type. + * + * @return The [StackNavigation] instance. + */ +fun AppNavigationProvider.getOrCreateTyped(): StackNavigation { + return getOrCreate() as StackNavigation +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultAppNavigationProvider.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultAppNavigationProvider.kt new file mode 100644 index 0000000000..76a8089d75 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultAppNavigationProvider.kt @@ -0,0 +1,13 @@ +package com.tangem.core.decompose.navigation + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.essenty.instancekeeper.InstanceKeeper + +internal class DefaultAppNavigationProvider : AppNavigationProvider, InstanceKeeper.Instance { + + private var navigation: StackNavigation? = null + + override fun getOrCreate(): StackNavigation { + return navigation ?: StackNavigation().also { navigation = it } + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt new file mode 100644 index 0000000000..50e3840442 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt @@ -0,0 +1,32 @@ +package com.tangem.core.decompose.navigation + +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.popWhile +import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.essenty.instancekeeper.InstanceKeeper + +internal class DefaultRouter( + private val navigationProvider: AppNavigationProvider, +) : Router, InstanceKeeper.Instance { + + private val navigation: StackNavigation + get() = navigationProvider.getOrCreate() + + @OptIn(ExperimentalDecomposeApi::class) + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + navigation.pushNew(route, onComplete) + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + navigation.pop(onComplete) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + navigation.popWhile( + predicate = { it != route }, + onComplete = onComplete, + ) + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/NavigationOwner.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/NavigationOwner.kt new file mode 100644 index 0000000000..d1f9c0bfa3 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/NavigationOwner.kt @@ -0,0 +1,17 @@ +package com.tangem.core.decompose.navigation + +/** + * Interface for owning navigation-related properties. + */ +interface NavigationOwner { + + /** + * The [Router] instance. + */ + val router: Router + + /** + * The [AppNavigationProvider] instance. + */ + val navigationProvider: AppNavigationProvider +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Route.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Route.kt new file mode 100644 index 0000000000..08d84f1a56 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Route.kt @@ -0,0 +1,6 @@ +package com.tangem.core.decompose.navigation + +/** + * Interface for a route in the application. + */ +interface Route \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt new file mode 100644 index 0000000000..3a9b7d93ca --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt @@ -0,0 +1,31 @@ +package com.tangem.core.decompose.navigation + +/** + * Interface for a router in the application. + * It provides methods for navigating through the application. + */ +interface Router { + + /** + * Pushes a new route to the navigation stack. + * + * @param route The route to push. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops the top route from the navigation stack. + * + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the specified route is found. + * + * @param route The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt new file mode 100644 index 0000000000..59546d42e0 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/DefaultUiMessageSender.kt @@ -0,0 +1,10 @@ +package com.tangem.core.decompose.ui + +internal class DefaultUiMessageSender( + private val handler: UiMessageHandler, +) : UiMessageSender { + + override fun send(message: UiMessage) { + handler.handleMessage(message) + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessage.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessage.kt new file mode 100644 index 0000000000..70be07d41c --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessage.kt @@ -0,0 +1,9 @@ +package com.tangem.core.decompose.ui + +/** + * Interface for a message that can be sent to a [UiMessageSender] and handled by a [UiMessageHandler]. + * + * @see UiMessageSender + * @see UiMessageHandler + */ +interface UiMessage \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageHandler.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageHandler.kt new file mode 100644 index 0000000000..8633b81eb9 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageHandler.kt @@ -0,0 +1,17 @@ +package com.tangem.core.decompose.ui + +/** + * Interface for handling UI messages. + * + * @see UiMessage + * @see UiMessageSender + */ +interface UiMessageHandler { + + /** + * Handles the given UI message. + * + * @param message The UI message to handle. + */ + fun handleMessage(message: UiMessage) +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageSender.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageSender.kt new file mode 100644 index 0000000000..9b58bbb09c --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageSender.kt @@ -0,0 +1,17 @@ +package com.tangem.core.decompose.ui + +/** + * Interface for sending messages to UI. + * + * @see UiMessage + * @see UiMessageHandler + * */ +interface UiMessageSender { + + /** + * Sends the given UI message. + * + * @param message The UI message to send. + * */ + fun send(message: UiMessage) +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageSenderOwner.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageSenderOwner.kt new file mode 100644 index 0000000000..f53e420e9c --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/ui/UiMessageSenderOwner.kt @@ -0,0 +1,12 @@ +package com.tangem.core.decompose.ui + +/** + * Interface for owning a [UiMessageSender]. + * */ +interface UiMessageSenderOwner { + + /** + * The [UiMessageSender] instance. + * */ + val messageSender: UiMessageSender +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ComponentCoroutineScope.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ComponentCoroutineScope.kt new file mode 100644 index 0000000000..dcfcb4aa7e --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ComponentCoroutineScope.kt @@ -0,0 +1,20 @@ +package com.tangem.core.decompose.utils + +import com.arkivanov.essenty.lifecycle.Lifecycle +import com.arkivanov.essenty.lifecycle.doOnDestroy +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +/** + + * [CoroutineDispatcherProvider.mainImmediate] dispatcher. + * */ +@Suppress("FunctionName") +internal fun ComponentCoroutineScope(lifecycle: Lifecycle, dispatchers: CoroutineDispatcherProvider): CoroutineScope { + val scope = CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob()) + lifecycle.doOnDestroy(scope::cancel) + + return scope +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ComponentScopeOwner.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ComponentScopeOwner.kt new file mode 100644 index 0000000000..75152213c6 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ComponentScopeOwner.kt @@ -0,0 +1,16 @@ +package com.tangem.core.decompose.utils + +import kotlinx.coroutines.CoroutineScope + +/** + * Interface for owning a component scope. + */ +interface ComponentScopeOwner { + + /** + * Provides access to the component's [CoroutineScope] instance. + * + * This scope is used for launching coroutines that are bound to the component's lifecycle. + */ + val componentScope: CoroutineScope +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/DispatchersOwner.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/DispatchersOwner.kt new file mode 100644 index 0000000000..94163474ff --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/DispatchersOwner.kt @@ -0,0 +1,14 @@ +package com.tangem.core.decompose.utils + +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +/** + * Interface for owning a [CoroutineDispatcherProvider]. + */ +interface DispatchersOwner { + + /** + * Provides access to the [CoroutineDispatcherProvider] instance. + */ + val dispatchers: CoroutineDispatcherProvider +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/TagsOwner.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/TagsOwner.kt new file mode 100644 index 0000000000..d7cd00ac80 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/TagsOwner.kt @@ -0,0 +1,12 @@ +package com.tangem.core.decompose.utils + +/** + * Interface for owning tags. + */ +interface TagsOwner { + + /** + * Provides access to the tags map instance. + */ + val tags: HashMap +} \ No newline at end of file diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts index d775494d39..410ef17d0e 100644 --- a/core/featuretoggles/build.gradle.kts +++ b/core/featuretoggles/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { /** Core modules */ implementation(projects.core.datasource) + implementation(projects.core.utils) testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 8f0373cabb..a4472cf056 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -11,20 +11,36 @@ "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "5.10.0" }, - { - "name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED", - "version": "5.8.0" - }, { "name": "LOCAL_USER_LOGS_ENABLED", - "version": "5.12.0" + "version": "5.13.0" }, { "name": "GENERATE_XPUB_ENABLED", - "version": "undefined" + "version": "5.12.0" }, { "name": "WC_SOLANA_TX_SIGN_ENABLED", "version": "undefined" + }, + { + "name": "TOKEN_LIST_LCE_ENABLED", + "version": "5.12.0" + }, + { + "name": "CARDANO_TOKENS_SUPPORT_ENABLED", + "version": "5.12.0" + }, + { + "name": "STAKING_ENABLED", + "version": "undefined" + }, + { + "name": "FULL_RESET_ENABLED", + "version": "5.12.0" + }, + { + "name": "DETAILS_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt index e67ca2a738..ca45c78a6b 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt @@ -1,16 +1,13 @@ package com.tangem.core.featuretoggle.di import android.content.Context -import com.squareup.moshi.Moshi -import com.squareup.moshi.adapter -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage import com.tangem.core.featuretoggle.version.DefaultVersionProvider import com.tangem.core.featuretoggles.BuildConfig -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module import dagger.Provides @@ -25,17 +22,12 @@ internal object FeatureTogglesManagerModule { @Provides @Singleton - @OptIn(ExperimentalStdlibApi::class) fun provideFeatureTogglesManager( @ApplicationContext context: Context, - assetReader: AssetReader, + assetLoader: AssetLoader, appPreferencesStore: AppPreferencesStore, ): FeatureTogglesManager { - val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() - val localFeatureTogglesStorage = LocalFeatureTogglesStorage( - assetReader = assetReader, - jsonAdapter = moshi.adapter(), - ) + val localFeatureTogglesStorage = LocalFeatureTogglesStorage(assetLoader) val versionProvider = DefaultVersionProvider(context) return if (BuildConfig.TESTER_MENU_ENABLED) { diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt index befc11c3b0..dbd92dee48 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt @@ -1,41 +1,28 @@ package com.tangem.core.featuretoggle.storage -import androidx.annotation.VisibleForTesting -import com.squareup.moshi.JsonAdapter import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH -import com.tangem.datasource.asset.AssetReader -import timber.log.Timber +import com.tangem.datasource.asset.loader.AssetLoader import kotlin.properties.Delegates /** * Storage implementation for storing local feature toggles. * Feature toggles are declared in file [LOCAL_CONFIG_PATH]. * - * @property assetReader asset reader - * @property jsonAdapter adapter for parsing local json config + * @property assetLoader asset loader * [REDACTED_AUTHOR] */ internal class LocalFeatureTogglesStorage( - private val assetReader: AssetReader, - private val jsonAdapter: JsonAdapter>, + private val assetLoader: AssetLoader, ) : FeatureTogglesStorage { override var featureToggles: List by Delegates.notNull() private set override suspend fun init() { - runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) } - .onSuccess { featureToggles = it } - .onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") } + featureToggles = assetLoader.loadList(LOCAL_CONFIG_PATH) } - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun getConfigPath() = LOCAL_CONFIG_PATH - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun getFeatureToggles(): Iterable = featureToggles - private companion object { const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config" } diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt index fae88c79f8..3950e2c649 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt +++ b/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt @@ -3,12 +3,11 @@ package com.tangem.core.featuretoggle.storage import android.annotation.SuppressLint import com.google.common.truth.Truth import com.squareup.moshi.JsonAdapter -import com.tangem.datasource.asset.AssetReader -import io.mockk.coEvery -import io.mockk.mockk -import io.mockk.verifyAll -import io.mockk.verifyOrder -import kotlinx.coroutines.ExperimentalCoroutinesApi +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.asset.reader.AssetReader +import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.Test import java.io.IOException @@ -16,75 +15,73 @@ import java.io.IOException /** [REDACTED_AUTHOR] */ -@OptIn(ExperimentalCoroutinesApi::class) @SuppressLint("CheckResult") internal class LocalFeatureTogglesStorageTest { private val assetReader = mockk() + private val moshi = mockk() private val jsonAdapter = mockk>>() - private val storage = LocalFeatureTogglesStorage(assetReader, jsonAdapter) + + // Impossible to mockk AssetLoader because it implement inline functions + private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi) + + private val storage = LocalFeatureTogglesStorage(assetLoader) @Test fun `successfully initialize storage`() = runTest { - coEvery { assetReader.readJson(storage.getConfigPath()) } returns json - coEvery { jsonAdapter.fromJson(json) } returns featureToggles + everyReadingJson() returns json + everyCreatingMoshiAdapter() returns jsonAdapter + everyMappingJson() returns featureToggles storage.init() - verifyOrder { - assetReader.readJson(storage.getConfigPath()) + coVerifyOrder { + assetReader.read(CONFIG_FILE_NAME) jsonAdapter.fromJson(json) } - Truth.assertThat(storage.getFeatureToggles()).containsExactlyElementsIn(featureToggles) + Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(featureToggles) } @Test fun `failure initialize storage if assetReader throws exception`() = runTest { - coEvery { assetReader.readJson(storage.getConfigPath()) } returns json - coEvery { jsonAdapter.fromJson(json) } throws IOException() + everyReadingJson() returns json + everyCreatingMoshiAdapter() returns jsonAdapter + everyMappingJson() throws IOException() storage.init() - verifyOrder { - assetReader.readJson(storage.getConfigPath()) + coVerifyOrder { + assetReader.read(CONFIG_FILE_NAME) jsonAdapter.fromJson(json) } - runCatching { storage.getFeatureToggles() } - .onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") } - .onFailure { - Truth - .assertThat(it) - .hasMessageThat() - .contains("Property featureToggles should be initialized before get.") - - Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) - } + Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList()) } @Test fun `failure initialize storage if jsonAdapter throws exception`() = runTest { - coEvery { assetReader.readJson(storage.getConfigPath()) } throws IOException() + everyReadingJson() throws IOException() storage.init() - verifyOrder { assetReader.readJson(storage.getConfigPath()) } + coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) } verifyAll(inverse = true) { jsonAdapter.fromJson(any()) } - runCatching { storage.getFeatureToggles() } - .onSuccess { throw IllegalStateException("featureToggles shouldn't be initialized") } - .onFailure { - Truth - .assertThat(it) - .hasMessageThat() - .contains("Property featureToggles should be initialized before get.") - - Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) - } + Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList()) } + private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) } + + private fun everyCreatingMoshiAdapter() = every { + val types = Types.newParameterizedType(List::class.java, FeatureToggle::class.java) + moshi.adapter>(types) + } + + private fun everyMappingJson() = every { jsonAdapter.fromJson(json) } + private companion object { + const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json" val json = """ [ diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index fc3c720d74..0c2f9e38e2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -4,16 +4,12 @@ Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. Обратиться в поддержку - Попробовать снова Эта функция недоступна в демонстрационном режиме Причина: %s Не могу отправить транзакцию Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. - Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки. - Ваши предложения отправлены - Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. @@ -28,8 +24,6 @@ Тёмная Светлая Как в системе - При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства - Системная Тема Настройки приложения Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" @@ -54,13 +48,21 @@ Вы уверены, что хотите это сделать? Смена кода доступа Код доступа будет изменен только на данной карте + Все карты выбранного кошелька сброшены до заводских настроек, вы можете создать новый кошелек + Сброс завершён + Хотите сбросить следующую карту от этого кошелька? + Сброс карты + Рекомендуем завершить процесс сброса всех карт кошелька + Вы сбросили не все карты Заводские настройки Тип безопасности Настройки карты - Ввиду особенностей сети Cardano при транзакции токена %1$s помимо комиссии сети будет списано %2$s - Для совершения транзакции %1$s, вам необходимо внести немного %2$s (%3$s), чтобы покрыть комиссию сети и минимальное значение ADA для отправки. - Недостаточно ADA для отправки токена - Вывод всего баланса ADA невозможен при наличии средств на токенах сети Cardano. Сначала выведите средства на ваших токенах. + Помимо сетевой комиссий, сеть Cardano взимает %1$s ADA при транзакции с токеном %2$s + Требования к транзакции Cardano + Чтобы совершить транзакцию %1$s, внесите некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA (рекомендуется 5 ADA) + Недостаточно ADA для транзакции + Вы должны поддерживать некоторое количество ADA, поскольку у вас на балансе есть токены в сети Cardano + Недостаточно ADA Принять Доступ запрещен Применить @@ -81,7 +83,6 @@ Создать Удалить Отключено - Отключить Готово Включить Включено @@ -99,6 +100,7 @@ Получить адреса К провайдеру Импортировать + Позже Заблокирован Основная сеть Сетевая комиссия @@ -115,7 +117,6 @@ Отклонить Перезагрузить Переименовать - Повторить Сохранить изменения Искать Поиск токенов @@ -139,7 +140,6 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно - Предупреждение Да Адрес контракта скопирован! Доступные сети @@ -186,7 +186,6 @@ Скрывать балансы жестом переворота Эмитент Подписано - Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно. Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -241,6 +240,7 @@ Требуется разрешение Условиями использования Токены не найдены. Пожалуйста, попробуйте другой запрос + ID: %s ID транзакции скопирован Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. @@ -257,10 +257,19 @@ Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции + Приложите карту #%s для сброса Приложите, чтобы отсканировать Приложите, чтобы подписать Приложите карту Вы обновили данные биометрии, отсканируйте свою карту для входа + Ваш баланс должен быть выше суммы комиссии для осуществления перевода + Недостаточно средств + У вас недостаточно Маны для этой транзакции. Пожалуйста, подождите, пока Мана восполнится. Ваш баланс маны равен %1$s/%2$s + Недостаточно Маны + Вы можете перевести только %s из-за ограничения Mana, установленного сетью Koinos + Лимит маны + Сеть Koinos использует Ману для оплаты комиссии сети. У вас есть %1$s/%2$s Mana + Уровень маны Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены Управление токенами Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту @@ -305,6 +314,7 @@ Введенные коды доступа не совпадают Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам Ошибка активации + Добавление токенов Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. Парольная фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов. @@ -388,7 +398,9 @@ Сортировка токенов Список Выбрать из галереи - Нравится наше приложение? + Настройки + Вы не предоставили доступ к вашей камере + Доступ к камере запрещен %1$s (%2$s) в сети %3$s Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. Участвовать @@ -443,13 +455,16 @@ Сканировать Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. Приготовьте свою карту - Уже содержится в введенном адресе + Уже содержится во введенном адресе Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена Сумма Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte. + Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана. (Приоритетная комиссия включена) + Приоритетная комиссия + Комиссия, которую пользователь может заплатить майнерам или валидаторам за ускорение включения его транзакции в блок. %1$s, %2$s Адрес Код назначения @@ -473,14 +488,11 @@ Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана. Всё Максимальная сумма - Комиссия не превысит - Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. - Допустим ввод только цифр - Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии + Комиссия не превысит + Недопустимый Memo Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Оставить %s Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе. Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. @@ -492,7 +504,9 @@ Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить. Сумма отправки не может быть менее %s + Оставить %s Уменьшить на %s + Уменьшить до %s Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. @@ -512,6 +526,7 @@ Нажмите на любое поле, чтобы изменить его Отправка %s Вы отправляете **%1$s**, включая комиссию сети %2$s + Вы отправляете **%1$s** и %2$s Отправка %s Всего %1$s и %2$s будет отправлено @@ -520,6 +535,9 @@ Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Неверный адрес Транзакция отправлена + Забыть кошелек + Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. + Имя Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -540,7 +558,6 @@ Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств - Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии Подтвердить Текущая транзакция Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. @@ -558,19 +575,23 @@ Балансы показаны Отменить Выбранная операция в данный момент недоступна. Попробуйте позже. - В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением. - У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. - Обмен %s не доступен. Но мы работаем над его добавлением. - В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением. - Выберите адрес + В данный момент покупка монеты %s недоступна. Следите за нашими обновлениями. + У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. + У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. + В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. + Продажа средств станет доступной после завершения транзакции(-ий) в сети %s + Отправка средств станет доступной после завершения транзакции(-ий) в сети %s + В данный момент продажа %s недоступна. Следите за нашими обновлениями. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. Скрыть %s Скрыть токен + Стейкинг позволяет вам зарабатывать %1$s и получать вознаграждения каждые %2$s дней + Зарабатывайте до %s вознаграждений за стейкинг ежегодно %1$s токен в сети %%image%% %2$s Токен в сети %%image%% %1$s - Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. + Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети Невозможно скрыть %s Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии @@ -599,6 +620,7 @@ Вы уверены, что хотите удалить этот кошелек? Произошла ошибка, пожалуйста, отсканируйте свою карту для входа Этот кошелек уже был сохранен, вы можете добавить другой + Кошелек с именем %s уже существует Имя кошелька Переименование кошелька Разблокировать все @@ -641,6 +663,7 @@ Сеть %s Адрес скопирован в буфер обмена Нет соединения с интернетом + Настройки кошелька Tangem Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. @@ -675,9 +698,10 @@ Возможно, данная карта - образец или подделка Ошибка проверки подлинности Ассоциировать - Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%.4f %s + Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять Ассоциируете свой токен + Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. @@ -697,12 +721,10 @@ Карта уже подписывала транзакции Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше Нравится Tangem? + Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его Необходима плата за аренду сети %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. Недостаточно %1$s для оплаты комиссии сети - Отправка средств станет доступной после завершения транзакции(-ий) в сети %s - Отправка средств станет доступной после завершения транзакции %s - Транзакция в обработке Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. Оповещение сети Солана Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6e41876896..0e761dde1b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,16 +4,12 @@ Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Request support - Try again This feature is disabled in Demo mode Reason: %s Can\'t send a transaction The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. - Thank you for your feedback. We will respond as soon as possible - Your suggestions were sent - Please try to tap the card exactly as shown in the animation or request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -29,8 +25,6 @@ Dark Light System default - If system is selected, the app will auto-adjust based on your device\'s system settings - System Theme App settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -53,13 +47,21 @@ Are you sure you want to do this? Change Access Code Access code will be changed on this card only + All cards in the selected wallet have been reset to factory settings. You can now create a new wallet. + Reset complete + Do you want to reset the next card in this wallet? + Card reset + We recommend completing the reset process for all cards in this wallet + You haven\'t reset all your cards Reset to Factory Settings Security Mode Card settings - Due to the peculiarities of the Cardano network, when transacting the %1$s token, in addition to the network commission, %2$s will be charged - To make a %1$s transaction, you must deposit some %2$s (%3$s) to cover the network fee and minimum ADA value - Insufficient ADA to token transfer - Withdrawal of the entire ADA balance is not possible if funds are available in Cardano network tokens. First, withdraw funds on your tokens. + In addition to network fee, the Cardano network charges %1$s ADA when transacting with the %2$s token + Cardano transaction requirements + To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended) + Insufficient ADA for token transfer + You must maintain some ADA because you have some tokens on the Cardano blockchain + Not enough ADA Accept Access denied Apply @@ -80,7 +82,6 @@ Create Delete Disabled - Disconnect Done Enable Enabled @@ -98,6 +99,7 @@ Get addresses Go to provider Import + Later Locked Main network Network fee @@ -114,7 +116,6 @@ Reject Reload Rename - Retry Save changes Search Search tokens @@ -138,7 +139,6 @@ I understand There was an error. Please try again. Unreachable - Warning Yes Contract address copied! Available networks @@ -185,7 +185,6 @@ Flip-to-Hide Balances Issuer Signed - If you forget the code you will lose access to your funds. Code recovery is not possible. Details Check your internet connection or switch to a different network Terms of service @@ -257,10 +256,19 @@ To change the access code tap the card as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation To create the wallet tap the card as shown above and do not remove until the end of the operation + Tap the card #%s of the wallet Tap to scan Tap to sign Tap the card You have updated biometrics, scan your card to enter + Your balance should be higher than the fee value to make a transfer + Not enough balance + You don\'t have enough Mana for this transaction. Please wait until the Mana is refilled. Your Mana balance is %1$s/%2$s + Not enough Mana + You can transfer only %s due to the Mana limit imposed by the Koinos network + Mana limit + The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana + Mana level To begin tracking your crypto assets and transactions, add tokens Manage tokens To access all the networks you need to scan the card @@ -305,6 +313,7 @@ Entered access code didn\'t match the initial access code Please repeat the operation. The card will be reset to factory settings. Activation error + Add tokens You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? The backup process is partly complete. You can\'t exit it now. The passphrase is an advanced security feature that crypto wallets use. It adds an extra word or phrase of your own choosing to your already existing recovery phrase to unlock a brand-new set of addresses. @@ -387,7 +396,6 @@ Settings You have not given access to your camera Camera access denied - Enjoying our app? %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. Participate @@ -444,9 +452,11 @@ Reason: %1$s\nCode: %2$s The transaction is not completed Amount - Base fee - Represents the part of the transaction fee that goes to the miner You can set your transaction fee by adjusting the value in the Satoshi per vByte field. + Max fee + This is the cost you are willing to pay for each unit of gas. The higher the gas price, the faster your transaction will be processed. (Priority fee included) + Priority fee + The fee that a user can pay to miners or validators to expedite the inclusion of their transaction in a block. %1$s, %2$s Address Destination Tag @@ -470,10 +480,8 @@ This is the cost you are willing to pay for each unit of gas. The higher the gas price, the faster your transaction will be processed. Max Maximum amount - Max fee - The fee that will be charged for your transaction. You can set your own value. - Numbers only for Destination Tag - Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level + Fee up to + Invalid Memo Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance @@ -490,13 +498,12 @@ The amount to send must be at least %s Leave %s Reduce by %s + Reduce to %s Kindly be aware that your transaction may experience delays under specific fee settings Transaction delays are possible Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Transaction limitation Optional - Priority fee - Represents the minimum gasUsed multiplier required for a transaction to be included in a block. This is the part of the transaction fee that is burnt. Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent Recipient @@ -511,6 +518,7 @@ Tap any field to change it Send %s You are sending **%1$s** including a network fee of %2$s + You are sending **%1$s** and %2$s Sending %s Total %1$s and %2$s will be sent @@ -520,6 +528,9 @@ Invalid address %1$s (%2$s) Transaction sent + Forget wallet + This will remove the wallet from the application. The wallet itself can be added again. + Name Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -540,7 +551,6 @@ Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds - Sending amount will be reduced to cover the selected fee level Approve Current transaction The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. @@ -558,19 +568,23 @@ Balances shown Undo This operation is currently unavailable. Please try again later. - The purchase of the %s is currently unavailable. But we are working on adding it. - You do not have funds to send. Top up your account to be able to send funds from it. - %s swap is not available. But we are working on adding it. - Sell of the %s coin is currently unavailable. But we are working on adding it. - Choose address + Buying %s is not available at the moment. Please check our updates. + You do not have funds to sell. Top up your account to be able to sell funds from it. + You do not have funds to send. Top up your account to be able to send funds from it. + Swapping %s is not available at the moment. Please check our updates. + Selling funds will be available once the pending transaction(s) in network %s is complete + Sending funds will be available once the pending transaction(s) in network %s is complete + Selling %s is not available at the moment. Please check our updates. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. Hide %s Hide token + Staking allows you to earn %1$s and get rewards every %2$s days + Earn up to %s staking rewards yearly %1$s token in %%image%% %2$s network Token in %%image%% %1$s network - The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. + The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees @@ -599,6 +613,7 @@ Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one + The wallet with name %s already exists Wallet name Rename Wallet Unlock all @@ -641,6 +656,7 @@ %s network Address was copied to clipboard No internet connection + Wallet settings Tangem Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. @@ -675,9 +691,10 @@ This card might be a production sample or counterfeit Authenticity check failed Associate - This token must be associated with your Hedera account before you can receive it. Association fee ~%.4f %s + This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s This token must be associated with your Hedera account before you can receive it Associate your token + Not enough %s. Top up your Hedera account to associate this token Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. @@ -695,12 +712,10 @@ Card has already signed transactions Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? + You must associate your token before receiving tokens Network rent fee required %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. Insufficient %1$s to cover network fee - Sending funds will be available once the pending transaction(s) in network %s is complete - Sending funds will be available once the %s transaction is complete - Transaction pending The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. Solana Network Alert Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3129c8917b..e7c5bfc4e7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { /** Project - Core */ implementation(projects.core.res) implementation(projects.core.utils) + implementation(projects.core.decompose) /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt new file mode 100644 index 0000000000..62e19d4af6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui + +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.theme.AppThemeModeHolder + +interface UiDependencies { + + val hapticManager: HapticManager + + val appThemeModeHolder: AppThemeModeHolder +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt new file mode 100644 index 0000000000..2e6354a2e2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.clipboard + +import androidx.compose.runtime.Stable + +/** + * Ready to use clipboard manager. + * Does not require context to work + */ +@Stable +interface ClipboardManager { + + /** Copies to clipboard [text] with optional [label] */ + fun setText(label: String = "", text: String) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 1e2301844b..bb17fcf7b6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -12,6 +13,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme // region TextButton @@ -319,17 +321,10 @@ private fun PrimaryButtonSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PrimaryButtonPreview_Light() { - TangemTheme { - PrimaryButtonSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun PrimaryButtonPreview_Dark() { - TangemTheme(isDark = true) { +private fun PrimaryButtonPreview() { + TangemThemePreview { PrimaryButtonSample() } } @@ -383,17 +378,10 @@ private fun SecondaryButtonSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SecondaryButtonPreview_Light() { - TangemTheme { - SecondaryButtonSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SecondaryButtonPreview_Dark() { - TangemTheme(isDark = true) { +private fun SecondaryButtonPreview() { + TangemThemePreview { SecondaryButtonSample() } } @@ -414,19 +402,11 @@ private fun TextButtonSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TextButtonPreview_LightTheme() { - TangemTheme { +private fun TextButtonPreview() { + TangemThemePreview { TextButtonSample() } } - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun TextButtonPreview_DarkTheme() { - TangemTheme(isDark = true) { - TextButtonSample() - } -} - // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt deleted file mode 100644 index 5654a037e7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt +++ /dev/null @@ -1,222 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.res.ButtonColorType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TextColorType -import com.tangem.core.ui.res.buttonColor -import com.tangem.core.ui.res.textColor - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -/** - * Primary button with an icon at the beginning of the layout - * - * @param modifier button modifier - * @param text button text - * @param iconResId button icon res id - * @param enabled controls the enabled state of the button - * @param onClick the lambda to be invoked when this button is pressed - * - * @see Figma component - */ -@Deprecated("Use PrimaryButtonIconRight instead") -@Composable -fun PrimaryStartIconButton( - text: String, - @DrawableRes iconResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, -) { - PrimaryButtonRow( - modifier = modifier, - enabled = enabled, - onClick = onClick, - content = { - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - ) - SpacerW8() - Text(text = text) - }, - ) -} - -/** - * Primary button with an icon at the end of the layout - * - * @param modifier button modifier - * @param text button text - * @param iconResId button icon res id - * @param enabled controls the enabled state of the button - * @param onClick the lambda to be invoked when this button is pressed - * - * @see Figma component - */ -@Deprecated("Use PrimaryButtonIconLeft instead") -@Composable -fun PrimaryEndIconButton( - text: String, - @DrawableRes iconResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, -) { - PrimaryButtonRow( - modifier = modifier, - enabled = enabled, - onClick = onClick, - content = { - Text(text = text) - SpacerW8() - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - ) - }, - ) -} - -@Composable -private fun PrimaryButtonRow( - enabled: Boolean, - onClick: () -> Unit, - content: @Composable (RowScope.() -> Unit), - modifier: Modifier = Modifier, -) { - Button( - onClick = onClick, - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size48), - enabled = enabled, - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.PRIMARY), - contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2), - disabledBackgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.DISABLED), - disabledContentColor = MaterialTheme.colors.textColor(type = TextColorType.DISABLED), - ), - content = content, - ) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 91dd9a2506..380c6bd97b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.valentinilk.shimmer.shimmer import kotlinx.collections.immutable.toImmutableList @@ -650,34 +652,19 @@ private fun CardsPreview() { } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Cards_InLightTheme() { - TangemTheme(isDark = false) { - CardsPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_InfoCardWithWarning_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_Cards() { + TangemThemePreview { CardsPreview() } } @Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Preview(widthDp = 328, heightDp = 48, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SimpleInfoCard_InLightTheme() { - TangemTheme(isDark = false) { - SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") - SmallInfoCardWithDisclaimer(startText = "Balance", endText = "0.4405434 BTC", disclaimer = "test") - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_SimpleInfoCard_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_SimpleInfoCard() { + TangemThemePreview { SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") SmallInfoCardWithDisclaimer(startText = "Balance", endText = "0.4405434 BTC", disclaimer = "test") } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt index 8d10e7486c..71b0550226 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -39,19 +41,11 @@ fun CurrencyPlaceholderIcon(id: String, modifier: Modifier = Modifier) { // region preview @Preview(showBackground = true, heightDp = 40, widthDp = 40) +@Preview(showBackground = true, heightDp = 40, widthDp = 40, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_CurrencyPlaceholderIcon_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_CurrencyPlaceholderIcon() { + TangemThemePreview { CurrencyPlaceholderIcon(id = "DAI") } } - -@Preview(showBackground = true, heightDp = 40, widthDp = 40) -@Composable -private fun Preview_CurrencyPlaceholderIcon_InDarkTheme() { - TangemTheme(isDark = true) { - CurrencyPlaceholderIcon(id = "DAI") - } -} - // endregion preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 658d6af9f6..18ca78091a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -26,6 +27,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleDialogTextField +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -450,33 +452,19 @@ private fun BasicDialogPreview() { } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SimpleOkDialog_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_SimpleOkDialog() { + TangemThemePreview { SimpleOkDialogPreview() } } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_BasicDialog_InLightTheme() { - TangemTheme(isDark = false) { - BasicDialogPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_SimpleOkDialog_InDarkTheme() { - TangemTheme(isDark = true) { - SimpleOkDialogPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_BasicDialog_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_BasicDialog() { + TangemThemePreview { BasicDialogPreview() } } @@ -498,17 +486,10 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun WarningBasicDialogPreview_Light() { - TangemTheme { - WarningBasicDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun WarningBasicDialogPreview_Dark() { - TangemTheme(isDark = true) { +private fun WarningBasicDialogPreview() { + TangemThemePreview { WarningBasicDialogSample() } } @@ -532,44 +513,19 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TextInputDialogPreview_Light() { - TangemTheme { +private fun TextInputDialogPreview() { + TangemThemePreview { TextInputDialogSample() } } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TextInputDialogPreview_Dark() { - TangemTheme(isDark = true) { - TextInputDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SelectorDialogPreview_Light( - @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, -) { - TangemTheme(isDark = false) { - SelectorDialog( - title = param.title, - items = param.items, - selectedItemIndex = param.selectedItemIndex, - confirmButton = DialogButton(title = "Cancel", onClick = {}), - onSelect = {}, - onDismissDialog = {}, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SelectorDialogPreview_Dark( - @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, -) { - TangemTheme(isDark = true) { +private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams) { + TangemThemePreview { SelectorDialog( title = param.title, items = param.items, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt index a646f37f33..5e3e78a06a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.heightIn @@ -13,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=WSV3AxC6zV1y0CHF-1) @@ -44,17 +46,10 @@ fun Notifier( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TangemNotifierPreview_Light(@PreviewParameter(NotifierProvider::class) text: String) { - TangemTheme(isDark = false) { - Notifier(text = text) - } -} - -@Preview -@Composable -private fun TangemNotifierPreview_Dark(@PreviewParameter(NotifierProvider::class) text: String) { - TangemTheme(isDark = true) { +private fun TangemNotifierPreview(@PreviewParameter(NotifierProvider::class) text: String) { + TangemThemePreview { Notifier(text = text) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 311fe1aae8..ddb5122ee1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.infiniteRepeatable @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.valentinilk.shimmer.* @@ -90,37 +92,24 @@ private val TangemShimmerColors: List // region preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ShimmersPreview() { - Column( - modifier = Modifier - .fillMaxWidth() - .background(TangemTheme.colors.background.primary), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), - ) { - RectangleShimmer( + TangemThemePreview { + Column( modifier = Modifier .fillMaxWidth() - .height(TangemTheme.dimens.size24), - ) - CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) + .background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), + ) { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size24), + ) + CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) + } } } - -@Preview(showBackground = true) -@Composable -private fun Shimmers_InLightTheme() { - TangemTheme(isDark = false) { - ShimmersPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Shimmers_InDarkTheme() { - TangemTheme(isDark = true) { - ShimmersPreview() - } -} - // endregion preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt index f8312aa659..055eaab6f7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState @@ -25,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -505,17 +507,10 @@ private fun OutlineTextFieldSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun OutlineTextFieldPreview_Light() { - TangemTheme { - OutlineTextFieldSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun OutlineTextFieldPreview_Dark() { - TangemTheme(isDark = true) { +private fun OutlineTextFieldPreview() { + TangemThemePreview { OutlineTextFieldSample() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index fe2a681d08..cee6370513 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -13,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -179,42 +181,29 @@ private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @C // region Preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun WarningsPreview() { - Column(modifier = Modifier.fillMaxWidth()) { - WarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - ) - SpacerH32() - ClickableWarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - onClick = {}, - ) - SpacerH32() - RefreshableWarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - onClick = {}, - ) + TangemThemePreview { + Column(modifier = Modifier.fillMaxWidth()) { + WarningCard( + title = "Exchange rate has expired", + description = "To access all the networks, you need to scan the card.", + ) + SpacerH32() + ClickableWarningCard( + title = "Exchange rate has expired", + description = "To access all the networks, you need to scan the card.", + onClick = {}, + ) + SpacerH32() + RefreshableWarningCard( + title = "Exchange rate has expired", + description = "To access all the networks, you need to scan the card.", + onClick = {}, + ) + } } } - -@Preview(showBackground = true) -@Composable -private fun Preview_Warning_InLightTheme() { - TangemTheme(isDark = false) { - WarningsPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_Warning_InDarkTheme() { - TangemTheme(isDark = true) { - WarningsPreview() - } -} - // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 1b910ca269..685358db13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -15,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -71,9 +73,10 @@ fun AppBarWithAdditionalButtons( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AppBarWithAdditionalButtons_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_AppBarWithAdditionalButtons() { + TangemThemePreview { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -89,27 +92,10 @@ private fun Preview_AppBarWithAdditionalButtons_InLightTheme() { } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AppBarWithAdditionalButtons_InDarkTheme() { - TangemTheme(isDark = true) { - AppBarWithAdditionalButtons( - text = "Tangem", - startButton = AdditionalButton( - iconRes = R.drawable.ic_scan_24, - onIconClicked = {}, - ), - endButton = AdditionalButton( - iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = {}, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_AppBarWithOnlyStartButtons() { + TangemThemePreview { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -121,37 +107,10 @@ private fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AppBarWithOnlyStartButtons_InDarkTheme() { - TangemTheme(isDark = true) { - AppBarWithAdditionalButtons( - text = "Tangem", - startButton = AdditionalButton( - iconRes = R.drawable.ic_scan_24, - onIconClicked = {}, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun Preview_AppBarWithOnlyEndButtons_InLightTheme() { - TangemTheme(isDark = false) { - AppBarWithAdditionalButtons( - text = "Tangem", - endButton = AdditionalButton( - iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = {}, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun Preview_AppBarWithOnlyEndButtons_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AppBarWithOnlyEndButtons() { + TangemThemePreview { AppBarWithAdditionalButtons( text = "Tangem", endButton = AdditionalButton( 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 afb57565f8..80d3014615 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -15,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -67,17 +69,10 @@ fun AppBarWithBackButton( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewAppBarWithBackButtonInLightTheme() { - TangemTheme(isDark = false) { - AppBarWithBackButton(text = "Title", onBackClick = {}) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun PreviewAppBarWithBackButtonInDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewAppBarWithBackButton() { + TangemThemePreview { AppBarWithBackButton(text = "Title", onBackClick = {}) } } \ No newline at end of file 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 405217433f..0006bdad9d 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.clickable @@ -11,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @Composable @@ -53,22 +55,10 @@ fun AppBarWithBackButtonAndIcon( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { - TangemTheme(isDark = false) { - AppBarWithBackButtonAndIcon( - text = "Title", - iconRes = R.drawable.ic_qrcode_scan_24, - onBackClick = {}, - onIconClick = {}, - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewAppBarWithBackButtonAndIcon() { + TangemThemePreview { AppBarWithBackButtonAndIcon( text = "Title", iconRes = R.drawable.ic_qrcode_scan_24, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index c65bf02622..fae3598a38 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background @@ -17,6 +18,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -98,34 +100,10 @@ fun AppBarWithBackButtonAndIconContent( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { - TangemTheme(isDark = false) { - AppBarWithBackButtonAndIconContent( - text = "Title", - onBackClick = {}, - iconContent = { - Row { - Icon( - painter = painterResource(id = R.drawable.ic_qrcode_scan_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Icon( - painter = painterResource(id = R.drawable.ic_flash_on_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - }, - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewAppBarWithBackButtonAndIcon() { + TangemThemePreview { AppBarWithBackButtonAndIconContent( text = "Title", subtitle = "Subtitle", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index 4dfd01d36d..ab6996b21d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -26,6 +26,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * App bar with close icon and search functionality @@ -228,7 +229,7 @@ private fun ExpandedSearchView( @Preview @Composable private fun CollapsedSearchViewPreview() { - TangemTheme { + TangemThemePreview { ExpandableSearchView( title = "Choose Token", onBackClick = {}, @@ -245,7 +246,7 @@ private fun CollapsedSearchViewPreview() { @Preview @Composable private fun ExpandedSearchViewPreview() { - TangemTheme { + TangemThemePreview { ExpandableSearchView( title = "Choose Token", onBackClick = {}, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt index f3d155f5a2..1f9dd3beca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.atoms +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable @@ -8,6 +9,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -52,17 +54,10 @@ private fun HandSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun HandPreview_Light() { - TangemTheme { - HandSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun HandPreview_Dark() { - TangemTheme(isDark = true) { +private fun HandPreview() { + TangemThemePreview { HandSample() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt index 7433327856..6a851a2abf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Constraints import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview sealed class TextEllipsis { @@ -201,7 +202,7 @@ private fun offsetEndEllipsisText( @Preview(widthDp = 200) @Composable private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) { - TangemTheme { + TangemThemePreview { EllipsisText( text = "11111111111111111111111111111111111111111111111111 END", ellipsis = ellipsis, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index 1057a43847..46317b571e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -1,6 +1,5 @@ package com.tangem.core.ui.components.bottomsheets.tokenreceive -import android.widget.Toast import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -9,10 +8,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboardManager @@ -26,9 +27,11 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rememberQrPainters +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme +import kotlinx.coroutines.launch @Composable fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) { @@ -40,117 +43,88 @@ fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) { var selectedAddress by remember { mutableStateOf(content.addresses.first()) } - Column( - modifier = Modifier - .verticalScroll(state = rememberScrollState()) - .padding( - start = TangemTheme.dimens.spacing24, - top = TangemTheme.dimens.spacing24, - end = TangemTheme.dimens.spacing24, - bottom = TangemTheme.dimens.spacing16, - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24), - ) { - QrCodeContent( - content = content, - onAddressChange = { selectedAddress = it }, - ) - Text( - text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + + val snackbarHostState = remember(::SnackbarHostState) + + ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) { + Column( + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + .padding( + start = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing24, + end = TangemTheme.dimens.spacing24, + bottom = TangemTheme.dimens.spacing16, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24), ) { - val clipboardManager = LocalClipboardManager.current - val hapticFeedback = LocalHapticFeedback.current - val context = LocalContext.current - SecondaryButtonIconStart( - modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_copy), - iconResId = R.drawable.ic_copy_24, - onClick = { - content.onCopyClick.invoke() - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clipboardManager.setText(AnnotatedString(selectedAddress.value)) - Toast.makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT).show() - }, - ) - SecondaryButtonIconStart( - modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_share), - iconResId = R.drawable.ic_share_24, - onClick = { - content.onShareClick.invoke() - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(selectedAddress.value) - }, - ) + QrCodeContent(content = content, onAddressChange = { selectedAddress = it }) + + DisclaimerText(text = content.name) + + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16)) { + CopyButton( + address = selectedAddress.value, + snackbarHostState = snackbarHostState, + onClick = content.onCopyClick, + modifier = Modifier.weight(1f), + ) + + ShareButton( + address = selectedAddress.value, + onClick = content.onShareClick, + modifier = Modifier.weight(1f), + ) + } } } } -@Suppress("LongMethod") +@Composable +private fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) { + Box { + content() + + CopiedTextSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing80), + ) + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) { val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value)) + val pagerState = rememberPagerState( initialPage = 0, initialPageOffsetFraction = 0f, - ) { - content.addresses.count() - } + pageCount = content.addresses::count, + ) LaunchedEffect(key1 = pagerState.currentPage) { onAddressChange.invoke(content.addresses[pagerState.currentPage]) } - HorizontalPager( - state = pagerState, - ) { currentPage -> - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), - ) { - Text( - text = stringResource( - R.string.receive_bottom_sheet_warning_message, - getName(content = content, index = pagerState.currentPage), - content.symbol, - content.network, - ), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h3, - ) - Image( - painter = qrCodes[currentPage], - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .size(TangemTheme.dimens.size248), - ) - Text( - text = content.addresses[currentPage].value, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.subtitle1, - ) - } + HorizontalPager(state = pagerState) { currentPage -> + QrCodePage( + content = content, + qrCodePainter = qrCodes[currentPage], + currentIndex = currentPage, + ) } if (pagerState.pageCount > 1) { val indicatorState = rememberLazyListState() val selectedColor = TangemTheme.colors.icon.primary1 val unselectedColor = TangemTheme.colors.icon.informative + LazyRow( - modifier = Modifier - .height(TangemTheme.dimens.size20), + modifier = Modifier.height(TangemTheme.dimens.size20), state = indicatorState, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, @@ -158,17 +132,14 @@ private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChang repeat(pagerState.pageCount) { iteration -> item(key = iteration) { val color by animateColorAsState( - if (pagerState.currentPage == iteration) selectedColor else unselectedColor, + targetValue = if (pagerState.currentPage == iteration) selectedColor else unselectedColor, + label = "", ) + Box( modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing4, - top = TangemTheme.dimens.spacing6, - end = TangemTheme.dimens.spacing4, - bottom = TangemTheme.dimens.spacing6, - ) - .background(color, CircleShape) + .padding(horizontal = TangemTheme.dimens.spacing4, vertical = TangemTheme.dimens.spacing6) + .background(color = color, shape = CircleShape) .size(TangemTheme.dimens.size7), ) } @@ -177,6 +148,41 @@ private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChang } } +@Composable +private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Painter, currentIndex: Int) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + ) { + Text( + text = stringResource( + R.string.receive_bottom_sheet_warning_message, + getName(content = content, index = currentIndex), + content.symbol, + content.network, + ), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + + Image( + painter = qrCodePainter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(TangemTheme.dimens.size248), + ) + + Text( + text = content.addresses[currentIndex].value, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle1, + ) + } +} + @Composable private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String { return if (content.addresses.size < 2) { @@ -184,4 +190,61 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String } else { "${content.addresses[index].displayName.resolveReference()} ${content.name}" } +} + +@Composable +private fun DisclaimerText(text: String) { + Text( + text = stringResource(R.string.receive_bottom_sheet_warning_message_full, text), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2, + ) +} + +@Composable +private fun CopyButton( + address: String, + snackbarHostState: SnackbarHostState, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val clipboardManager = LocalClipboardManager.current + val coroutineScope = rememberCoroutineScope() + val resources = LocalContext.current.resources + + SecondaryButtonIconStart( + modifier = modifier, + text = stringResource(id = R.string.common_copy), + iconResId = R.drawable.ic_copy_24, + onClick = { + onClick() + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clipboardManager.setText(AnnotatedString(address)) + + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getString(R.string.wallet_notification_address_copied), + ) + } + }, + ) +} + +@Composable +private fun ShareButton(address: String, onClick: () -> Unit, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + SecondaryButtonIconStart( + modifier = modifier, + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + onClick() + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(address) + }, + ) } \ No newline at end of file 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 a6c3ee8402..715a630ed3 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth @@ -15,6 +16,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -41,21 +43,12 @@ fun HorizontalActionChips( // region Preview @Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_HorizontalActionChips_Light( +private fun Preview_HorizontalActionChips( @PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips, ) { - TangemTheme(isDark = false) { - HorizontalActionChips(buttons = buttons.buttons) - } -} - -@Preview(widthDp = 360) -@Composable -private fun Preview_HorizontalActionChips_Dark( - @PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips, -) { - TangemTheme(isDark = true) { + TangemThemePreview { HorizontalActionChips(buttons = buttons.buttons) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index 60bc4a690f..15f640bdbf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons +import android.content.res.Configuration import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -14,6 +15,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -90,17 +92,10 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SmallButton_Light() { - TangemTheme(isDark = false) { - ButtonsSample() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_SmallButton_Dark() { - TangemTheme(isDark = true) { +private fun Preview_SmallButton() { + TangemThemePreview { ButtonsSample() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt index 6b9347623a..c93d92643e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.TextReference * @property text text * @property iconResId icon resource id * @property onClick lambda be invoked when action component is clicked + * @property onLongClick lambda be invoked when action component is long clicked * @property enabled enabled * @property dimContent determines whether the button content will be dimmed. This property will be ignored if [enabled] * is `false`. @@ -19,6 +20,7 @@ data class ActionButtonConfig( val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, + val onLongClick: (() -> TextReference?)? = null, val enabled: Boolean = true, val dimContent: Boolean = false, ) \ No newline at end of file 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 3d0b87e26a..60bc6e7e12 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 @@ -1,8 +1,11 @@ package com.tangem.core.ui.components.buttons.actions +import android.content.res.Configuration +import android.widget.Toast import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon @@ -13,6 +16,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.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +27,7 @@ import com.tangem.core.ui.components.SpacerW8 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 /** * Rounded action button @@ -72,6 +77,7 @@ fun ActionButton( ) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun Button( config: ActionButtonConfig, @@ -83,13 +89,24 @@ private fun Button( targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled, label = "Update background color", ) - + val context = LocalContext.current Row( modifier = modifier .heightIn(min = TangemTheme.dimens.size36) .clip(shape) .background(color = backgroundColor) - .clickable(enabled = config.enabled, onClick = config.onClick) + .combinedClickable( + enabled = config.enabled, + onClick = config.onClick, + onLongClick = { + val toastReference = config.onLongClick?.invoke() + toastReference?.let { + Toast + .makeText(context, toastReference.resolveReference(context.resources), Toast.LENGTH_SHORT) + .show() + } + }, + ) .padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24) .padding(vertical = TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.Center, @@ -133,33 +150,19 @@ private fun Button( } @Preview(group = "RoundedActionButton", showBackground = true) +@Preview(group = "RoundedActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_RoundedActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = false) { - RoundedActionButton(state) - } -} - -@Preview(group = "RoundedActionButton", showBackground = true) -@Composable -private fun Preview_RoundedActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = true) { +private fun Preview_RoundedActionButton(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { + TangemThemePreview { RoundedActionButton(state) } } @Preview(group = "ActionButton", showBackground = true) +@Preview(group = "ActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = false) { - ActionButton(state) - } -} - -@Preview(group = "ActionButton", showBackground = true) -@Composable -private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = true) { +private fun Preview_ActionButton(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { + TangemThemePreview { ActionButton(state) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index f3cbfca76d..0bfeb822a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons.segmentedbutton +import android.content.res.Configuration import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -20,6 +21,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -102,29 +104,12 @@ inline fun SegmentedButtons( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SegmentedButtonsPreview_Light( +private fun SegmentedButtonsPreview( @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, ) { - TangemTheme { - SegmentedButtons( - config = config, - onClick = {}, - ) { - Text( - text = it.text, - modifier = Modifier.padding(TangemTheme.dimens.spacing16), - ) - } - } -} - -@Preview -@Composable -private fun SegmentedButtonsPreview_Dark( - @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, -) { - TangemTheme(isDark = true) { + TangemThemePreview { SegmentedButtons( config = config, onClick = {}, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt index 76d5c9d094..13462984c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt @@ -4,15 +4,12 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.DefaultCurrencyIcon - -private const val GRAY_SCALE_SATURATION = 0f -private const val GRAY_SCALE_ALPHA = 0.4f +import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA +import com.tangem.core.ui.utils.GrayscaleColorFilter /** * Simple icon from network @@ -46,7 +43,4 @@ fun FiatIcon( }, modifier = modifier, ) -} - -private val GrayscaleColorFilter: ColorFilter - get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) \ No newline at end of file +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt index 07d299f583..559bc7bde8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt @@ -10,14 +10,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme - -private const val GRAY_SCALE_SATURATION = 0f -private const val GRAY_SCALE_ALPHA = 0.4f -private const val NORMAL_ALPHA = 1f +import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA +import com.tangem.core.ui.utils.GrayscaleColorFilter +import com.tangem.core.ui.utils.NORMAL_ALPHA /** * Cryptocurrency icon with network badge @@ -112,7 +109,4 @@ private fun BoxScope.ContentIconContainer( @Composable private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) -} - -private val GrayscaleColorFilter: ColorFilter - get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) \ No newline at end of file +} \ No newline at end of file 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 7c185e50d6..c985d43cfc 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 @@ -27,6 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -179,7 +180,7 @@ private fun AmountTextFieldPreview( @PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData, ) { var text by remember { mutableStateOf(amount.value.orEmpty()) } - TangemTheme { + TangemThemePreview { AmountTextField( value = text, decimals = amount.decimals, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt index 23f26292ee..c4f107369a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.icons.identicon +import android.content.res.Configuration import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size @@ -13,6 +14,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme private const val GAP_WIDTH = 1f @@ -62,9 +64,10 @@ fun IdentIcon(address: String, modifier: Modifier = Modifier) { //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun IdentIconPreview_Light() { - TangemTheme { +private fun IdentIconPreview() { + TangemThemePreview { IdentIcon( address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", modifier = Modifier @@ -72,17 +75,4 @@ private fun IdentIconPreview_Light() { ) } } - -@Preview -@Composable -private fun IdentIconPreview_Dark() { - TangemTheme(isDark = true) { - IdentIcon( - address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", - modifier = Modifier - .size(TangemTheme.dimens.size40), - ) - } -} - //endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index 6310a11f5a..2bd8311866 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -123,40 +125,36 @@ private fun InputRowApproxItem( //region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowApproxPreview_Light() { - TangemTheme { - InputRowApprox( - leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title USD"), - leftSubtitle = TextReference.Str("Left subtitle USD"), - leftTitleEllipsisOffset = 3, - rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"), - rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"), - rightTitleEllipsisOffset = 3, - modifier = Modifier - .background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowApproxPreview_Dark() { - TangemTheme(isDark = true) { - InputRowApprox( - leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"), - leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"), - leftTitleEllipsisOffset = 3, - rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title USD"), - rightSubtitle = TextReference.Str("Right subtitle USD"), - rightTitleEllipsisOffset = 3, - modifier = Modifier - .background(TangemTheme.colors.background.action), - ) +private fun InputRowApproxPreview() { + TangemThemePreview { + Column { + InputRowApprox( + leftIcon = TokenIconState.Loading, + leftTitle = TextReference.Str("Left title USD"), + leftSubtitle = TextReference.Str("Left subtitle USD"), + leftTitleEllipsisOffset = 3, + rightIcon = TokenIconState.Loading, + rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"), + rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"), + rightTitleEllipsisOffset = 3, + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + InputRowApprox( + leftIcon = TokenIconState.Loading, + leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"), + leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"), + leftTitleEllipsisOffset = 3, + rightIcon = TokenIconState.Loading, + rightTitle = TextReference.Str("Right title USD"), + rightSubtitle = TextReference.Str("Right subtitle USD"), + rightTitleEllipsisOffset = 3, + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + } } } //endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index 620e3346cf..3f711de362 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -154,30 +156,12 @@ private fun InnerIcon(imageUrl: String) { //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowBestRatePreview_Light( +private fun InputRowBestRatePreview( @PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData, ) { - TangemTheme { - InputRowBestRate( - imageUrl = "", - title = data.title, - titleExtra = data.titleExtra, - subtitle = data.subtitle, - showTag = data.showTag, - onIconClick = data.iconClick, - modifier = Modifier - .background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowBestRatePreview_Dark( - @PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowBestRate( imageUrl = "", title = data.title, 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 e41ac02d17..4222d238c0 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -19,6 +20,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -94,27 +96,12 @@ fun InputRowDefault( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowDefaultPreview_Light( +private fun InputRowDefaultPreview( @PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData, ) { - TangemTheme { - InputRowDefault( - title = TextReference.Str(data.title), - text = TextReference.Str(data.text), - iconRes = data.iconRes, - showDivider = data.showDivider, - modifier = Modifier.background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowDefaultPreview_Dark( - @PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowDefault( title = TextReference.Str(data.title), text = TextReference.Str(data.text), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt index f81372c3da..4d1490891f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -106,28 +108,12 @@ fun InputRowEnter( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowEnterPreview_Light( +private fun InputRowEnterPreview( @PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData, ) { - TangemTheme { - InputRowEnter( - title = TextReference.Str(data.title), - text = data.text, - iconRes = data.iconRes, - showDivider = data.showDivider, - onValueChange = {}, - modifier = Modifier.background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowEnterPreview_Dark( - @PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowEnter( title = TextReference.Str(data.title), text = data.text, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt index 25debb16dc..c9e01fd85f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -92,28 +94,12 @@ fun InputRowEnterInfo( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowEnterInfoPreview_Light( +private fun InputRowEnterInfoPreview( @PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData, ) { - TangemTheme { - InputRowEnterInfo( - title = data.title, - text = data.text, - info = data.info, - showDivider = data.showDivider, - onValueChange = {}, - modifier = Modifier.background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowEnterInfoPreview_Dark( - @PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowEnterInfo( title = data.title, text = data.text, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt index 1754c3b8bc..35ba18ad8a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -120,31 +122,12 @@ fun InputRowImage( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowInputEnterInfoPreview_Light( +private fun InputRowInputEnterInfoPreview( @PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData, ) { - TangemTheme { - InputRowImage( - title = data.title, - modifier = Modifier.background(TangemTheme.colors.background.action), - subtitle = data.subtitle, - caption = data.caption, - tokenIconState = data.iconState, - iconRes = data.actionIconRes, - onIconClick = {}, - showNetworkIcon = false, - showDivider = data.showDivider, - ) - } -} - -@Preview -@Composable -private fun InputRowImagePreview_Dark( - @PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowImage( title = data.title, modifier = Modifier.background(TangemTheme.colors.background.action), 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 ccb48a3e6c..b1df3b3e40 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -23,6 +24,7 @@ import com.tangem.core.ui.components.inputrow.inner.PasteButton 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 kotlinx.coroutines.delay /** @@ -54,6 +56,7 @@ fun InputRowRecipient( isError: Boolean = false, showDivider: Boolean = false, isLoading: Boolean = false, + isValuePasted: Boolean = false, ) { val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning @@ -93,6 +96,7 @@ fun InputRowRecipient( placeholder = placeholder, onValueChange = onValueChange, singleLine = singleLine, + isValuePasted = isValuePasted, modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) .weight(1f) @@ -152,11 +156,12 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) { //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowRecipientPreview_Light( +private fun InputRowRecipientPreview( @PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData, ) { - TangemTheme { + TangemThemePreview { InputRowRecipient( value = value.value, title = TextReference.Res(R.string.send_recipient), @@ -172,27 +177,6 @@ private fun InputRowRecipientPreview_Light( } } -@Preview -@Composable -private fun InputRowRecipientPreview_Dark( - @PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData, -) { - TangemTheme(isDark = true) { - InputRowRecipient( - value = value.value, - title = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_optional_field), - error = TextReference.Str("Error"), - isLoading = value.isLoading, - isError = value.isError, - showDivider = true, - onValueChange = {}, - onPasteClick = {}, - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) - } -} - private data class InputRowRecipientPreviewData( val value: String, val isError: Boolean, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt index 8ae6b1b386..94f75c6a56 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -79,9 +81,10 @@ fun InputRowRecipientDefault( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowRecipientPreview_Light() { - TangemTheme { +private fun InputRowRecipientPreview() { + TangemThemePreview { InputRowRecipientDefault( value = "0x391316d97a07027a0702c8A002c8A0C25d8470", title = TextReference.Res(R.string.send_recipient), @@ -90,17 +93,4 @@ private fun InputRowRecipientPreview_Light() { ) } } - -@Preview -@Composable -private fun InputRowRecipientPreview_Dark() { - TangemTheme(isDark = true) { - InputRowRecipientDefault( - value = "0x391316d97a07027a0702c8A002c8A0C25d8470", - title = TextReference.Res(R.string.send_recipient), - showDivider = false, - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) - } -} //endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index 653cd250e6..af8a6761b3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION /** @@ -100,7 +101,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) { @Composable private fun PasteButtonPreview() { var isVisible by remember { mutableStateOf(true) } - TangemTheme { + TangemThemePreview { PasteButton( isPasteButtonVisible = isVisible, onClick = { isVisible = !isVisible }, 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 873d6dbcb2..606416093a 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.marketprice +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -19,6 +20,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter @@ -203,23 +205,13 @@ private fun QuoteTimeStatus() { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_MarketPriceBlock_Light( +private fun Preview_MarketPriceBlock( @PreviewParameter(WalletMarketPriceBlockStateProvider::class) state: MarketPriceBlockState, ) { - TangemTheme(isDark = false) { - MarketPriceBlock(state = state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun Preview_MarketPriceBlock_Dark( - @PreviewParameter(WalletMarketPriceBlockStateProvider::class) - state: MarketPriceBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { MarketPriceBlock(state = state) } } 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 4a71c9c99c..c5b3d10a9b 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 @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.notifications +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image @@ -29,6 +30,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState @@ -123,7 +125,9 @@ private fun MainContent( Icon( iconResId = iconResId, tint = iconTint, - modifier = Modifier.align(alignment = Alignment.CenterVertically), + modifier = Modifier + .size(size = TangemTheme.dimens.size20) + .align(alignment = Alignment.CenterVertically), ) SpacerW(width = TangemTheme.dimens.spacing10) @@ -282,22 +286,13 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Notification_Light( +private fun Preview_Notification( @PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig, ) { - TangemTheme(isDark = false) { - Notification(config) - } -} - -@Preview -@Composable -private fun Preview_Notification_Dark( - @PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { Notification(config) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 0c48f4b767..076c60da41 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.notifications +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -32,6 +33,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.TangemColorPalette.Dark6 import com.tangem.core.ui.res.TangemColorPalette.Light4 +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -153,21 +155,12 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun NotificationWithBackgroundPreview_Light( +private fun NotificationWithBackgroundPreview( @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, ) { - TangemTheme { - NotificationWithBackground(config = config) - } -} - -@Preview -@Composable -private fun NotificationWithBackgroundPreview_Dark( - @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { NotificationWithBackground(config = config) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 0c2a785acc..36c44d81b3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH28 import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Simple clickable action row, without input and icon @@ -66,7 +67,7 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod @Composable private fun SimpleActionRowPreview() { Column { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { SimpleActionRow( title = "Title", description = "Description", @@ -75,7 +76,7 @@ private fun SimpleActionRowPreview() { SpacerH28() - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { SimpleActionRow( title = "Title", description = "Description", 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 3802d115d4..c1e06e7270 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 @@ -1,5 +1,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 @@ -22,6 +23,7 @@ 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.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @Composable @@ -140,25 +142,10 @@ private fun RowScope.SelectorValueContent( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SelectorRowItemPreview_Light() { - TangemTheme { - SelectorRowItem( - titleRes = R.string.common_fee_selector_option_slow, - iconRes = R.drawable.ic_tortoise_24, - preDot = TextReference.Str("1000 ETH"), - postDot = TextReference.Str("1000 $"), - ellipsizeOffset = 4, - isSelected = true, - onSelect = { }, - ) - } -} - -@Preview -@Composable -private fun SelectorRowItemPreview_Dark() { - TangemTheme(isDark = true) { +private fun SelectorRowItemPreview() { + TangemThemePreview { SelectorRowItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt new file mode 100644 index 0000000000..35cc8f6903 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt @@ -0,0 +1,114 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarData +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.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +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 + +/** + * Snackbar to inform the user about copying text to the clipboard + * + * @param message message + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbar(message: TextReference, modifier: Modifier = Modifier) { + BaseSnackbar(message = message, modifier = modifier) +} + +/** + * Snackbar to inform the user about copying text to the clipboard. + * + * @param snackbarData this is needed to better support Material3.SnackbarHost, but only supports the message field + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbar(snackbarData: SnackbarData, modifier: Modifier = Modifier) { + BaseSnackbar(message = stringReference(snackbarData.visuals.message), modifier = modifier) +} + +@Composable +private fun BaseSnackbar(message: TextReference, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(color = TangemTheme.colors.icon.secondary, shape = TangemTheme.shapes.roundedCorners8) + .heightIn(min = TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing14, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + MarkIcon() + + MessageText(text = message, modifier = Modifier.weight(weight = 1f, fill = false)) + } +} + +@Composable +private fun MarkIcon() { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size20), + tint = TangemTheme.colors.icon.accent, + ) +} + +@Composable +private fun MessageText(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveReference(), + modifier = modifier, + color = TangemTheme.colors.text.disabled, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_CopiedTextSnackbar( + @PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference, +) { + TangemTheme(isDark = false) { + CopiedTextSnackbar(message = message) + } +} + +private class CopiedTextSnackbarDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + stringReference(value = "Copied!"), + stringReference(value = "Contract address copied!"), + stringReference(value = "Coooooooooooontract addreeeeeeeeeeeeeeeess coooooooooooooooopied!"), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt new file mode 100644 index 0000000000..3cff3dd5c0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt @@ -0,0 +1,25 @@ +package com.tangem.core.ui.components.snackbar + +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * SnackbarHost to inform the user about copying text to the clipboard + * Based on Material3 component. It's best way to show [CopiedTextSnackbar]. + * + * @param hostState snackbar host state + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { + SnackbarHost(hostState = hostState, modifier = modifier) { + CopiedTextSnackbar(snackbarData = it) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt new file mode 100644 index 0000000000..70cacc3a82 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt @@ -0,0 +1,84 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Tangem snackbar. + * Based on Material3 component. It can be presented as one line or multi lines snackbar – depends on text length. + * + * @param data snackbar data + * @param modifier modifier + * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { + Snackbar( + modifier = modifier, + action = { + ActionButton(label = data.visuals.actionLabel, onClick = data::performAction) + }, + actionOnNewLine = actionOnNewLine, + shape = TangemTheme.shapes.roundedCorners8, + containerColor = TangemTheme.colors.icon.secondary, + ) { + MessageText(text = data.visuals.message) + } +} + +@Composable +private fun ActionButton(label: String?, onClick: () -> Unit) { + if (!label.isNullOrBlank()) { + TextButton( + onClick = onClick, + colors = ButtonDefaults.textButtonColors( + contentColor = TangemTheme.colors.text.primary2, + ), + content = { + Text( + text = label, + maxLines = 1, + style = TangemTheme.typography.button, + ) + }, + ) + } +} + +@Composable +private fun MessageText(text: String) { + Text( + text = text, + color = TangemTheme.colors.text.disabled, + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body2, + ) +} + +/** + * IMPORTANT! + * Preview doesn't work correctly, check on device or start [TangemSnackbarHost]'s preview in interactive mode * + */ +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) { + TangemThemePreview { + TangemSnackbar(data = model.snackbarData, actionOnNewLine = model.actionOnNewLine) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt new file mode 100644 index 0000000000..d1cd229f6d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt @@ -0,0 +1,48 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Tangem snackbar host. + * Based on Material3 component. It's best way to show [TangemSnackbar]. + * + * @param hostState snackbar host state + * @param modifier modifier + * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { + SnackbarHost(hostState = hostState, modifier = modifier) { data -> + TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine) + } +} + +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) { + TangemThemePreview { + val snackbarHostState = remember(::SnackbarHostState) + + TangemSnackbarHost(hostState = snackbarHostState, actionOnNewLine = model.actionOnNewLine) + + LaunchedEffect(key1 = null) { + snackbarHostState.showSnackbar(visuals = model.snackbarData.visuals) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt new file mode 100644 index 0000000000..e2bdbf3ecf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components.snackbar + +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider + +internal data class TangemSnackbarModel(val snackbarData: SnackbarData, val actionOnNewLine: Boolean) + +internal class TangemSnackbarModelProvider : CollectionPreviewParameterProvider( + listOf( + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Button", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Very loooooooooooong single-line description.", + actionLabel = "Button", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Very looooooong button name", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Button", + actionOnNewLine = true, + ), + createTangemSnackbarModel( + message = "Very loooooooooooong single-line description.", + actionLabel = "Button", + actionOnNewLine = true, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Very looooooong button name", + actionOnNewLine = true, + ), + ), +) { + + companion object { + + fun createTangemSnackbarModel( + message: String, + actionLabel: String, + actionOnNewLine: Boolean, + ): TangemSnackbarModel { + return TangemSnackbarModel( + snackbarData = createSnackbarData(message, actionLabel), + actionOnNewLine = actionOnNewLine, + ) + } + + private fun createSnackbarData(message: String, actionLabel: String): SnackbarData { + return object : SnackbarData { + + override val visuals: SnackbarVisuals + get() = object : SnackbarVisuals { + override val message: String = message + override val actionLabel: String = actionLabel + override val duration: SnackbarDuration = SnackbarDuration.Short // Never-mind + override val withDismissAction: Boolean = false // Never-mind + } + + override fun dismiss() = Unit + override fun performAction() = Unit + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 271e85f311..e43769432c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -33,6 +34,7 @@ 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 import java.util.UUID /** @@ -317,23 +319,12 @@ private fun LockedContent(modifier: Modifier = Modifier) { ) } -@Preview +@Preview(showBackground = true, widthDp = 368) +@Preview(showBackground = true, widthDp = 368, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TransactionItem_LightTheme( - @PreviewParameter(TransactionItemStateProvider::class) state: TransactionState, -) { - TangemTheme(isDark = false) { - Transaction(state = state, isBalanceHidden = false) - } -} - -@Preview -@Composable -private fun Preview_TransactionItem_DarkTheme( - @PreviewParameter(TransactionItemStateProvider::class) state: TransactionState, -) { - TangemTheme(isDark = true) { - Transaction(state = state, isBalanceHidden = false) +private fun Preview_TransactionItem(@PreviewParameter(TransactionItemStateProvider::class) state: TransactionState) { + TangemThemePreview { + Transaction(state = state, isBalanceHidden = false, modifier = Modifier.padding(TangemTheme.dimens.spacing20)) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt index 62c98e0361..aea7aaa822 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -15,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat @@ -61,23 +63,10 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie // region Previews @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TransactionDoneTitlePreview_Light() { - TangemTheme { - TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = 0, - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16), - ) - } -} - -@Preview -@Composable -private fun TransactionDoneTitlePreview_Dark() { - TangemTheme(isDark = true) { +private fun TransactionDoneTitlePreview() { + TangemThemePreview { TransactionDoneTitle( titleRes = R.string.sent_transaction_sent_title, date = 0, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt index b2b02d9e1a..e0298ceaed 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -11,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import java.util.UUID @@ -42,19 +44,10 @@ internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TransactionsBlockGroupTitle_Light() { - TangemTheme(isDark = false) { - TxHistoryGroupTitle( - config = TxHistoryItemState.GroupTitle(title = "Today", itemKey = UUID.randomUUID().toString()), - ) - } -} - -@Preview -@Composable -private fun Preview_TransactionsBlockGroupTitle_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TransactionsBlockGroupTitle() { + TangemThemePreview { TxHistoryGroupTitle( config = TxHistoryItemState.GroupTitle(title = "Today", itemKey = UUID.randomUUID().toString()), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt index 352c19d181..3a7f89969c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,6 +13,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -59,17 +61,10 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TransactionsBlockTitle_Light() { - TangemTheme(isDark = false) { - TxHistoryTitle(onExploreClick = {}) - } -} - -@Preview -@Composable -private fun Preview_TransactionsBlockTitle_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TransactionsBlockTitle() { + TangemThemePreview { TxHistoryTitle(onExploreClick = {}) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt index c2b7368924..a6d96d536d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions.empty +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -87,20 +89,11 @@ private fun PairButtons(state: EmptyTransactionsBlockState.ButtonsState.PairButt @Composable @Preview(widthDp = 360, showBackground = true) -private fun EmptyTransactionBlock_Light( +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun EmptyTransactionBlockPreview( @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, ) { - TangemTheme { - EmptyTransactionBlock(state = state) - } -} - -@Composable -@Preview(widthDp = 360, showBackground = true) -private fun EmptyTransactionBlock_Dark( - @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { EmptyTransactionBlock(state = state) } } 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 1a95df0142..33a8bd032e 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 @@ -59,7 +59,7 @@ fun getActiveIconRes(blockchainId: String): Int { "pls", "pls/test" -> R.drawable.img_pls_22 "zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22 "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 - "manta", "manta/test" -> R.drawable.img_manta_22 + "manta-pacific", "manta/test" -> R.drawable.img_manta_22 "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22 "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22 "mantle", "mantle/test" -> R.drawable.img_mantle_22 @@ -67,6 +67,9 @@ fun getActiveIconRes(blockchainId: String): Int { "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 "base" -> R.drawable.img_base_22 + "joystream" -> R.drawable.img_joystream_22 + "koinos", "koinos/test" -> R.drawable.img_koinos_22 + "bittensor" -> R.drawable.img_bittensor_22 else -> R.drawable.ic_alert_24 } } @@ -127,7 +130,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "pls", "pls/test" -> R.drawable.img_pls_22 "zksync", "zksync/test" -> R.drawable.img_zksync_22 "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 - "manta-network", "manta-network/test" -> R.drawable.img_manta_22 + "manta-pacific", "manta-pacific/test" -> R.drawable.img_manta_22 "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.img_polygon_22 "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22 "mantle", "mantle/test" -> R.drawable.img_mantle_22 @@ -135,6 +138,9 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 "base" -> R.drawable.img_base_22 + "joystream" -> R.drawable.img_joystream_22 + "koinos", "koinos/test" -> R.drawable.img_koinos_22 + "bittensor" -> R.drawable.img_bittensor_22 else -> R.drawable.ic_alert_24 } } @@ -192,7 +198,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "pls" -> R.drawable.img_pls_22 "zksync-ethereum" -> R.drawable.img_zksync_22 "moonbeam" -> R.drawable.img_moonbeam_22 - "manta-network-ethereum" -> R.drawable.img_manta_22 + "manta-pacific" -> R.drawable.img_manta_22 "polygon-zkevm-ethereum" -> R.drawable.img_polygon_22 "moonriver" -> R.drawable.img_moonriver_22 "mantle" -> R.drawable.img_mantle_22 @@ -200,6 +206,9 @@ fun getActiveIconResByCoinId(coinId: String): Int { "taraxa" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 "base" -> R.drawable.img_base_22 + "joystream" -> R.drawable.img_joystream_22 + "koinos", "koinos/test" -> R.drawable.img_koinos_22 + "bittensor" -> R.drawable.img_bittensor_22 else -> R.drawable.ic_alert_24 } } @@ -260,7 +269,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "pls", "pls/test" -> R.drawable.ic_pls_22 "zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22 "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 - "manta", "manta/test" -> R.drawable.ic_manta_22 + "manta-pacific", "manta/test" -> R.drawable.ic_manta_22 "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22 "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22 "mantle", "mantle/test" -> R.drawable.ic_mantle_22 @@ -268,6 +277,9 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 "base", "base/test" -> R.drawable.ic_base_22 + "joystream" -> R.drawable.ic_joystream_22 + "koinos", "koinos/test" -> R.drawable.ic_koinos_22 + "bittensor" -> R.drawable.ic_bittensor_22 else -> R.drawable.ic_alert_24 } } @@ -328,7 +340,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "pls", "pls/test" -> R.drawable.ic_pls_22 "zksync", "zksync/test" -> R.drawable.ic_zksync_22 "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 - "manta-network", "manta-network/test" -> R.drawable.ic_manta_22 + "manta-pacific", "manta-pacific/test" -> R.drawable.ic_manta_22 "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.ic_polygon_22 "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22 "mantle", "mantle/test" -> R.drawable.ic_mantle_22 @@ -336,6 +348,9 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 "base", "base/test" -> R.drawable.ic_base_22 + "joystream" -> R.drawable.ic_joystream_22 + "koinos", "koinos/test" -> R.drawable.ic_koinos_22 + "bittensor" -> R.drawable.ic_bittensor_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/haptic/HapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt new file mode 100644 index 0000000000..b93c08bd26 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.haptic + +import androidx.compose.runtime.Stable + +@Stable +interface HapticManager { + + fun vibrateShort() + + fun vibrateMeduim() + + fun vibrateLong() +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt new file mode 100644 index 0000000000..876ee7a05d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.haptic + +object MockHapticManager : HapticManager { + + override fun vibrateShort() { + /** Intentionnaly do nothing */ + } + + override fun vibrateMeduim() { + /** Intentionnaly do nothing */ + } + + override fun vibrateLong() { + /** Intentionnaly do nothing */ + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt new file mode 100644 index 0000000000..f500f27e90 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt @@ -0,0 +1,47 @@ +package com.tangem.core.ui.message + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.ui.UiMessage +import com.tangem.core.ui.extensions.TextReference + +/** + * Event that is used to show a message in the UI. + * + * @see EventMessageHandler + * */ +@Immutable +sealed interface EventMessage : UiMessage + +/** + * Shows a snackbar. + * + * @param message The message to show. + * @param duration The duration of the snackbar. + * @param actionLabel The label of the action button. + * @param action The action to perform when the action button is clicked. + * */ +data class SnackbarMessage( + val message: TextReference, + val duration: SnackbarDuration = SnackbarDuration.Short, + val actionLabel: TextReference? = null, + val action: (() -> Unit)? = null, +) : EventMessage + +/** + * Shows a [content] in the UI. + * + * @param content The content to show. + * */ +data class ContentMessage(val content: Content) : EventMessage { + + @Stable + fun interface Content { + + @Suppress("ComposableFunctionName", "TopLevelComposableFunctions") + @Composable + operator fun invoke(onDismiss: () -> Unit) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt new file mode 100644 index 0000000000..41475b987a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageEffect.kt @@ -0,0 +1,43 @@ +package com.tangem.core.ui.message + +import android.content.Context +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.extensions.resolveReference + +@Composable +fun EventMessageEffect(messageHandler: EventMessageHandler, snackbarHostState: SnackbarHostState) { + val messageEvent by messageHandler.collectAsState() + val context = LocalContext.current + var contentMessage: ContentMessage? by remember { mutableStateOf(value = null) } + + EventEffect(event = messageEvent) { message -> + when (message) { + is ContentMessage -> { + contentMessage = message + } + is SnackbarMessage -> { + showSnackbar(snackbarHostState, message, context) + } + } + } + + contentMessage?.content?.invoke( + onDismiss = { contentMessage = null }, + ) +} + +private suspend fun showSnackbar(snackbarHostState: SnackbarHostState, message: SnackbarMessage, context: Context) { + val result = snackbarHostState.showSnackbar( + message = message.message.resolveReference(context.resources), + actionLabel = message.actionLabel?.resolveReference(context.resources), + duration = message.duration, + ) + + if (result == SnackbarResult.ActionPerformed) { + message.action?.invoke() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt new file mode 100644 index 0000000000..0f4d117dab --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt @@ -0,0 +1,27 @@ +package com.tangem.core.ui.message + +import com.tangem.core.decompose.ui.UiMessage +import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Message handler that is used to show or remove an [EventMessage] in the UI. + */ +class EventMessageHandler( + private val events: MutableStateFlow> = MutableStateFlow(consumedEvent()), +) : UiMessageHandler, StateFlow> by events { + + override fun handleMessage(message: UiMessage) { + if (message !is EventMessage) return + + events.value = triggeredEvent(message, ::consumeEvent) + } + + private fun consumeEvent() { + events.value = consumedEvent() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColorType.kt deleted file mode 100644 index 2e45dc7577..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColorType.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.material.Colors -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.res.TangemColorPalette.Dark5 -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.DarkGreen -import com.tangem.core.ui.res.TangemColorPalette.Light1 -import com.tangem.core.ui.res.TangemColorPalette.Light2 -import com.tangem.core.ui.res.TangemColorPalette.MagicMint -import com.tangem.core.ui.res.TangemColorPalette.Meadow - -@Deprecated("Use TangemTheme.colors") -enum class ButtonColorType(val lightColor: Color, val darkColor: Color) { - PRIMARY(lightColor = Dark6, darkColor = Light1), - SECONDARY(lightColor = Light2, darkColor = Dark5), - DISABLED(lightColor = Light2, darkColor = Dark6), - POSITIVE(lightColor = Meadow, darkColor = Meadow), - POSITIVE_DISABLED(lightColor = MagicMint, darkColor = DarkGreen), -} - -@Composable -fun Colors.buttonColor(type: ButtonColorType): Color = if (IS_SYSTEM_IN_DARK_THEME) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt deleted file mode 100644 index 42b0e53057..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.material.Colors -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.res.TangemColorPalette.Amaranth -import com.tangem.core.ui.res.TangemColorPalette.Azure -import com.tangem.core.ui.res.TangemColorPalette.Black -import com.tangem.core.ui.res.TangemColorPalette.Dark1 -import com.tangem.core.ui.res.TangemColorPalette.Dark2 -import com.tangem.core.ui.res.TangemColorPalette.Dark4 -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemColorPalette.Light5 -import com.tangem.core.ui.res.TangemColorPalette.Mustard -import com.tangem.core.ui.res.TangemColorPalette.Tangerine -import com.tangem.core.ui.res.TangemColorPalette.White - -@Deprecated("Use TangemTheme.colors") -enum class IconColorType(val lightColor: Color, val darkColor: Color) { - PRIMARY1(lightColor = Black, darkColor = White), - PRIMARY2(lightColor = White, darkColor = Dark6), - SECONDARY(lightColor = Dark2, darkColor = Dark1), - INFORMATIVE(lightColor = Light5, darkColor = Dark2), - INACTIVE(lightColor = Light4, darkColor = Dark4), - ACCENT(lightColor = Azure, darkColor = Azure), - WARNING(lightColor = Amaranth, darkColor = Amaranth), - ATTENTION(lightColor = Tangerine, darkColor = Mustard), -} - -@Composable -fun Colors.iconColor(type: IconColorType): Color = if (IS_SYSTEM_IN_DARK_THEME) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 79a2e16273..55321fa0b2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -51,6 +51,7 @@ data class TangemDimens internal constructor( val size11: Dp = 11.dp, val size12: Dp = 12.dp, val size14: Dp = 14.dp, + val size15: Dp = 15.dp, val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, @@ -103,6 +104,7 @@ data class TangemDimens internal constructor( val spacing10: Dp = 10.dp, val spacing12: Dp = 12.dp, val spacing14: Dp = 14.dp, + val spacing15: Dp = 15.dp, val spacing16: Dp = 16.dp, val spacing18: Dp = 18.dp, val spacing20: Dp = 20.dp, @@ -131,6 +133,7 @@ data class TangemDimens internal constructor( val spacing92: Dp = 92.dp, val spacing94: Dp = 94.dp, val spacing96: Dp = 96.dp, + val spacing108: Dp = 108.dp, val spacing154: Dp = 154.dp, // endregion Spacing ) \ 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 78d91538e0..4f3287a86d 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,6 +4,8 @@ import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.runtime.* +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.MockHapticManager // TODO: use isSystemInDarkTheme() for automatic color detection internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false @@ -13,6 +15,7 @@ fun TangemTheme( isDark: Boolean = false, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, + hapticManager: HapticManager = MockHapticManager, content: @Composable () -> Unit, ) { val themeColors = if (isDark) darkThemeColors() else lightThemeColors() @@ -29,6 +32,7 @@ fun TangemTheme( LocalTangemDimens provides dimens, LocalTangemShapes provides shapes, LocalIsInDarkTheme provides isDark, + LocalHapticManager provides hapticManager, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -196,4 +200,8 @@ private val LocalTangemShapes = staticCompositionLocalOf { error("No TangemShapes provided") } -val LocalIsInDarkTheme = staticCompositionLocalOf { false } \ No newline at end of file +val LocalIsInDarkTheme = staticCompositionLocalOf { false } + +val LocalHapticManager = staticCompositionLocalOf { + error("No HapticManager provided") +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt new file mode 100644 index 0000000000..2a9e0caf65 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -0,0 +1,21 @@ +package com.tangem.core.ui.res + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable + +@Composable +fun TangemThemePreview( + isDark: Boolean? = null, + typography: TangemTypography = TangemTheme.typography, + dimens: TangemDimens = TangemTheme.dimens, + content: @Composable () -> Unit, +) { + val isDarkTheme = isDark ?: isSystemInDarkTheme() + + TangemTheme( + isDark = isDarkTheme, + typography = typography, + dimens = dimens, + content = content, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt index eb48e1cd50..9c029684fe 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.sp @@ -23,6 +24,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.SemiBold, letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 44f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val h1: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -30,6 +35,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 44f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val h2: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -37,6 +46,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.18f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 32f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val h3: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -44,6 +57,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val subtitle1: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -51,13 +68,21 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val subtitle2: TextStyle = TextStyle( fontFamily = RobotoFamily, fontSize = 14.sp, fontWeight = FontWeight.Medium, - letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), + letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val body1: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -65,6 +90,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val body2: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -72,6 +101,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0.25f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val button: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -79,6 +112,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val caption1: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -86,6 +123,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val caption2: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -93,6 +134,10 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Normal, letterSpacing = TextUnit(value = 0.4f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), val overline: TextStyle = TextStyle( fontFamily = RobotoFamily, @@ -100,5 +145,9 @@ data class TangemTypography internal constructor( fontWeight = FontWeight.Medium, letterSpacing = TextUnit(value = 1.5f, type = TextUnitType.Sp), lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TextColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TextColorType.kt deleted file mode 100644 index b093f4f7af..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TextColorType.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.material.Colors -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.res.TangemColorPalette.Amaranth -import com.tangem.core.ui.res.TangemColorPalette.Dark1 -import com.tangem.core.ui.res.TangemColorPalette.Dark2 -import com.tangem.core.ui.res.TangemColorPalette.Dark3 -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemColorPalette.Light5 -import com.tangem.core.ui.res.TangemColorPalette.Meadow -import com.tangem.core.ui.res.TangemColorPalette.Mustard -import com.tangem.core.ui.res.TangemColorPalette.Tangerine -import com.tangem.core.ui.res.TangemColorPalette.White - -@Deprecated("Use TangemTheme.colors") -enum class TextColorType(val lightColor: Color, val darkColor: Color) { - PRIMARY1(lightColor = Dark6, darkColor = White), - PRIMARY2(lightColor = White, darkColor = Dark6), - SECONDARY(lightColor = Dark2, darkColor = Light5), - TERTIARY(lightColor = Dark1, darkColor = Dark1), - DISABLED(lightColor = Light4, darkColor = Dark3), - ACCENT(lightColor = Meadow, darkColor = Meadow), - WARNING(lightColor = Amaranth, darkColor = Amaranth), - ATTENTION(lightColor = Tangerine, darkColor = Mustard), - CONSTANT_WHITE(lightColor = White, darkColor = White), -} - -@Composable -fun Colors.textColor(type: TextColorType): Color = if (IS_SYSTEM_IN_DARK_THEME) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index f2eade80ca..4c2faae280 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.apptheme.model.AppThemeMode /** @@ -22,9 +22,9 @@ import com.tangem.domain.apptheme.model.AppThemeMode internal interface ComposeScreen { /** - * The holder for managing the current application theme mode. + * The holder for ui dependencies. */ - val appThemeModeHolder: AppThemeModeHolder + val uiDependencies: UiDependencies /** * The screen modifier. @@ -53,9 +53,12 @@ internal interface ComposeScreen { internal fun ComposeScreen.createComposeView(context: Context): ComposeView { return ComposeView(context).apply { setContent { - val appThemeMode by appThemeModeHolder.appThemeMode + val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode - TangemTheme(isDark = shouldUseDarkTheme(appThemeMode)) { + TangemTheme( + isDark = shouldUseDarkTheme(appThemeMode), + hapticManager = uiDependencies.hapticManager, + ) { ScreenContent(modifier = screenModifier) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt new file mode 100644 index 0000000000..8548896b31 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.test + +object TestTags { + const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" + const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" + const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" + + const val WALLET_SCREEN = "WALLET_SCREEN_CONTAINER" +} \ No newline at end of file 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 index b724559114..0dbdded548 100644 --- 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 @@ -13,9 +13,14 @@ object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = "—" const val CAN_BE_LOWER_SIGN = "<" + private val FORMAT_THRESHOLD = BigDecimal("0.01") private const val TEMP_CURRENCY_CODE = "USD" + private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") + private const val FIAT_MARKET_DEFAULT_DIGITS = 2 + private const val FIAT_MARKET_EXTENDED_DIGITS = 6 + fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -40,6 +45,39 @@ object BigDecimalFormatter { } } + fun formatCryptoAmountShorted( + cryptoAmount: BigDecimal?, + cryptoCurrency: String, + decimals: Int, + locale: Locale = Locale.getDefault(), + ): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + + val formatter = if (cryptoAmount.isMoreThanThreshold()) { + NumberFormat.getNumberInstance(locale).apply { + maximumFractionDigits = 2 + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + } else { + NumberFormat.getNumberInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.DOWN + } + } + + return formatter.format(cryptoAmount).let { + if (cryptoCurrency.isEmpty()) { + it + } else { + it + "\u2009$cryptoCurrency" + } + } + } + fun formatCryptoAmountUncapped( cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency, @@ -82,8 +120,43 @@ object BigDecimalFormatter { val formatterCurrency = getCurrency(fiatCurrencyCode) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency - maximumFractionDigits = 2 - minimumFractionDigits = 2 + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + return if (fiatAmount.isLessThanThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter.format(FIAT_FORMAT_THRESHOLD) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), + ) + } + } else { + formatter.format(fiatAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + } + + 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.isLessThanThreshold()) { + 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 } @@ -130,6 +203,8 @@ object BigDecimalFormatter { fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol" + private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD + private fun getCurrency(code: String): Currency { return runCatching { Currency.getInstance(code) } .getOrElse { e -> @@ -141,4 +216,6 @@ object BigDecimalFormatter { } } } + + private fun BigDecimal.isLessThanThreshold() = 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/GrayscaleUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt new file mode 100644 index 0000000000..f3edb75b60 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.utils + +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix + +const val GRAY_SCALE_SATURATION = 0f +const val GRAY_SCALE_ALPHA = 0.4f +const val NORMAL_ALPHA = 1f + +val GrayscaleColorFilter: ColorFilter + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_bittensor_22.xml b/core/ui/src/main/res/drawable/ic_bittensor_22.xml new file mode 100644 index 0000000000..f625ce0f50 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_bittensor_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_chat.xml b/core/ui/src/main/res/drawable/ic_chat_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_chat.xml rename to core/ui/src/main/res/drawable/ic_chat_24.xml diff --git a/app/src/main/res/drawable/ic_comment.xml b/core/ui/src/main/res/drawable/ic_comment_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_comment.xml rename to core/ui/src/main/res/drawable/ic_comment_24.xml diff --git a/app/src/main/res/drawable/ic_discord.xml b/core/ui/src/main/res/drawable/ic_discord_24.xml similarity index 97% rename from app/src/main/res/drawable/ic_discord.xml rename to core/ui/src/main/res/drawable/ic_discord_24.xml index 6502165e77..b1f5039116 100644 --- a/app/src/main/res/drawable/ic_discord.xml +++ b/core/ui/src/main/res/drawable/ic_discord_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/app/src/main/res/drawable/ic_facebook.xml b/core/ui/src/main/res/drawable/ic_facebook_24.xml similarity index 94% rename from app/src/main/res/drawable/ic_facebook.xml rename to core/ui/src/main/res/drawable/ic_facebook_24.xml index 1db025e42c..eeb0ea2747 100644 --- a/app/src/main/res/drawable/ic_facebook.xml +++ b/core/ui/src/main/res/drawable/ic_facebook_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/app/src/main/res/drawable/ic_github.xml b/core/ui/src/main/res/drawable/ic_github_24.xml similarity index 97% rename from app/src/main/res/drawable/ic_github.xml rename to core/ui/src/main/res/drawable/ic_github_24.xml index daa1de7021..8e105d74a6 100644 --- a/app/src/main/res/drawable/ic_github.xml +++ b/core/ui/src/main/res/drawable/ic_github_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/app/src/main/res/drawable/ic_instagram.xml b/core/ui/src/main/res/drawable/ic_instagram_24.xml similarity index 95% rename from app/src/main/res/drawable/ic_instagram.xml rename to core/ui/src/main/res/drawable/ic_instagram_24.xml index 76744cf7f5..a1b00c2ca5 100644 --- a/app/src/main/res/drawable/ic_instagram.xml +++ b/core/ui/src/main/res/drawable/ic_instagram_24.xml @@ -8,12 +8,12 @@ android:pathData="M2.4,2.4h19.2v19.2h-19.2z"/> + android:fillColor="#000" /> + android:fillColor="#000" /> + android:fillColor="#000" /> diff --git a/core/ui/src/main/res/drawable/ic_joystream_22.xml b/core/ui/src/main/res/drawable/ic_joystream_22.xml new file mode 100644 index 0000000000..91f187f81c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_joystream_22.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_koinos_22.xml b/core/ui/src/main/res/drawable/ic_koinos_22.xml new file mode 100644 index 0000000000..3d3ad5087a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_koinos_22.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_linkedin.xml b/core/ui/src/main/res/drawable/ic_linkedin_24.xml similarity index 95% rename from app/src/main/res/drawable/ic_linkedin.xml rename to core/ui/src/main/res/drawable/ic_linkedin_24.xml index 2df3cb834a..5f82e64300 100644 --- a/app/src/main/res/drawable/ic_linkedin.xml +++ b/core/ui/src/main/res/drawable/ic_linkedin_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/app/src/main/res/drawable/ic_reddit.xml b/core/ui/src/main/res/drawable/ic_reddit_24.xml similarity index 98% rename from app/src/main/res/drawable/ic_reddit.xml rename to core/ui/src/main/res/drawable/ic_reddit_24.xml index 2a1e04a004..61919b1686 100644 --- a/app/src/main/res/drawable/ic_reddit.xml +++ b/core/ui/src/main/res/drawable/ic_reddit_24.xml @@ -5,6 +5,6 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_telegram.xml b/core/ui/src/main/res/drawable/ic_telegram_24.xml similarity index 95% rename from app/src/main/res/drawable/ic_telegram.xml rename to core/ui/src/main/res/drawable/ic_telegram_24.xml index 020ee3bc71..d2d923c5a3 100644 --- a/app/src/main/res/drawable/ic_telegram.xml +++ b/core/ui/src/main/res/drawable/ic_telegram_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/app/src/main/res/drawable/ic_text.xml b/core/ui/src/main/res/drawable/ic_text_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_text.xml rename to core/ui/src/main/res/drawable/ic_text_24.xml diff --git a/app/src/main/res/drawable/ic_twitter.xml b/core/ui/src/main/res/drawable/ic_twitter_24.xml similarity index 92% rename from app/src/main/res/drawable/ic_twitter.xml rename to core/ui/src/main/res/drawable/ic_twitter_24.xml index 1c3cb6cfef..cd94ecb5b6 100644 --- a/app/src/main/res/drawable/ic_twitter.xml +++ b/core/ui/src/main/res/drawable/ic_twitter_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/app/src/main/res/drawable/ic_walletconnect.xml b/core/ui/src/main/res/drawable/ic_wallet_connect_24.xml similarity index 96% rename from app/src/main/res/drawable/ic_walletconnect.xml rename to core/ui/src/main/res/drawable/ic_wallet_connect_24.xml index d4b4c99942..a4378111f1 100644 --- a/app/src/main/res/drawable/ic_walletconnect.xml +++ b/core/ui/src/main/res/drawable/ic_wallet_connect_24.xml @@ -2,6 +2,6 @@ android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/drawable/ic_youtube.xml b/core/ui/src/main/res/drawable/ic_youtube_24.xml similarity index 95% rename from app/src/main/res/drawable/ic_youtube.xml rename to core/ui/src/main/res/drawable/ic_youtube_24.xml index 6d5ade9d08..7eb2833038 100644 --- a/app/src/main/res/drawable/ic_youtube.xml +++ b/core/ui/src/main/res/drawable/ic_youtube_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000" /> diff --git a/core/ui/src/main/res/drawable/img_bittensor_22.xml b/core/ui/src/main/res/drawable/img_bittensor_22.xml new file mode 100644 index 0000000000..4c7f09c9e7 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_bittensor_22.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/img_joystream_22.xml b/core/ui/src/main/res/drawable/img_joystream_22.xml new file mode 100644 index 0000000000..2168e5839e --- /dev/null +++ b/core/ui/src/main/res/drawable/img_joystream_22.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_koinos_22.xml b/core/ui/src/main/res/drawable/img_koinos_22.xml new file mode 100644 index 0000000000..2364704dc9 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_koinos_22.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt index a2533147f7..ce1a6d2470 100644 --- a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt +++ b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt @@ -40,25 +40,4 @@ fun BigDecimal.toFormattedCurrencyString( ) val formattedCurrency = currency?.let { " $it" } ?: "" return "$formattedAmount$formattedCurrency" -} - -fun BigDecimal.toFiatString( - rateValue: BigDecimal, - fiatCurrencyName: String, - formatWithSpaces: Boolean = false, -): String { - val fiatValue = rateValue.multiply(this) - val formatter = NumberFormat.getInstance(Locale.getDefault()) as? DecimalFormat - val df = formatter?.apply { - maximumFractionDigits = 2 - minimumFractionDigits = 2 - isGroupingUsed = true - this.roundingMode = roundingMode - } - val formatted = if (formatWithSpaces) { - "${df?.format(fiatValue)} $fiatCurrencyName" - } else { - "${df?.format(fiatValue)}$fiatCurrencyName" - } - return formatted } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt index f15bce01ea..53cfb7ed29 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt @@ -8,6 +8,7 @@ import javax.inject.Inject interface CoroutineDispatcherProvider { val main: CoroutineDispatcher + val mainImmediate: CoroutineDispatcher val io: CoroutineDispatcher val default: CoroutineDispatcher val single: CoroutineDispatcher @@ -15,6 +16,7 @@ interface CoroutineDispatcherProvider { class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider { override val main: CoroutineDispatcher = Dispatchers.Main + override val mainImmediate: CoroutineDispatcher = Dispatchers.Main.immediate override val io: CoroutineDispatcher = Dispatchers.IO override val default: CoroutineDispatcher = Dispatchers.Default override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() @@ -22,6 +24,7 @@ class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcher class TestingCoroutineDispatcherProvider( override val main: CoroutineDispatcher = Dispatchers.Unconfined, + override val mainImmediate: CoroutineDispatcher = Dispatchers.Unconfined, override val io: CoroutineDispatcher = Dispatchers.Unconfined, override val default: CoroutineDispatcher = Dispatchers.Unconfined, override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(), diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index ebbe1dc50a..cfa0536804 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -10,6 +10,13 @@ suspend fun runCatching(dispatcher: CoroutineDispatcher, block: suspend () - } } +suspend fun waitForDelay(delay: Long, block: suspend CoroutineScope.() -> R): R = coroutineScope { + val minWaitingTime = async { delay(delay) } + val actionInvoke = async { block() } + minWaitingTime.await() + actionInvoke.await() +} + class Debouncer { private var debounceJob: Job? = null diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt index 9930c5cb77..6ab240ea41 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -18,4 +18,9 @@ fun Collection.isSingleItem(): Boolean = this.size == 1 */ fun Collection.copy(): Collection { return this.map { it } +} + +inline fun List.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? { + val index = indexOfFirst(predicate) + return if (index == -1) null else index } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/version/AppVersionProvider.kt b/core/utils/src/main/java/com/tangem/utils/version/AppVersionProvider.kt new file mode 100644 index 0000000000..23e26bebd1 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/version/AppVersionProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.version + +interface AppVersionProvider { + + val versionName: String + + val versionCode: Int +} \ No newline at end of file diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index 872f5ce751..eb8abfb89e 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -26,8 +26,6 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.data.source.preferences) - implementation(projects.domain.card) implementation(projects.domain.models) } \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index 0efec70af4..7e629bb338 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -6,7 +6,6 @@ import com.tangem.common.UserCodeType import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.data.card.sdk.CardSdkProvider -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.ProductType @@ -14,14 +13,10 @@ import com.tangem.domain.models.scan.ProductType * Implementation of repository for managing of CardSDK config * * @property cardSdkProvider CardSDK instance provider - * @property preferencesDataSource application shared preferences * [REDACTED_AUTHOR] */ -internal class DefaultCardSdkConfigRepository( - private val cardSdkProvider: CardSdkProvider, - private val preferencesDataSource: PreferencesDataSource, -) : CardSdkConfigRepository { +internal class DefaultCardSdkConfigRepository(private val cardSdkProvider: CardSdkProvider) : CardSdkConfigRepository { @Deprecated("Use CardSdkConfigRepository's methods instead of this property") override val sdk: TangemSdk @@ -58,8 +53,6 @@ internal class DefaultCardSdkConfigRepository( } } - override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes - override fun getCommonSigner(cardId: String?) = CommonSigner( tangemSdk = sdk, cardId = cardId, diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt index a526073cd2..eb53f5dea1 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -3,7 +3,6 @@ package com.tangem.data.card.di import com.tangem.data.card.DefaultCardRepository import com.tangem.data.card.DefaultCardSdkConfigRepository import com.tangem.data.card.sdk.CardSdkProvider -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -19,14 +18,8 @@ internal object CardDataModule { @Provides @Singleton - fun provideCardSdkConfigRepository( - cardSdkProvider: CardSdkProvider, - preferencesDataSource: PreferencesDataSource, - ): CardSdkConfigRepository { - return DefaultCardSdkConfigRepository( - cardSdkProvider = cardSdkProvider, - preferencesDataSource = preferencesDataSource, - ) + fun provideCardSdkConfigRepository(cardSdkProvider: CardSdkProvider): CardSdkConfigRepository { + return DefaultCardSdkConfigRepository(cardSdkProvider = cardSdkProvider) } @Provides diff --git a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt index b481d8e846..837e169c8b 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt @@ -4,7 +4,9 @@ import arrow.core.raise.Raise import arrow.core.raise.recover import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError +import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber +import kotlin.time.Duration /** * A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to @@ -28,6 +30,28 @@ value class ApiResponseRaise( } } +/** + * Attempts to execute an API call safely, providing error handling and a timeout. + * + * @param T The return type of the API call and the function. + * @param timeoutMillis The timeout in milliseconds for the API call. Default is 30 seconds. + * @param call The API call block to execute. + * @param onError A function to handle errors and return a fallback value of type [T]. + * + * @return The result of the API call or the fallback value provided by [onError] if an error occurs. + */ +suspend inline fun safeApiCallWithTimeout( + timeoutMillis: Duration = with(Duration) { 30.seconds }, + crossinline call: suspend ApiResponseRaise.() -> T, + crossinline onError: suspend (ApiResponseError) -> T, +): T = safeApiCall( + call = { + withTimeoutOrNull(timeoutMillis) { call() } + ?: raise(ApiResponseError.TimeoutException) + }, + onError = onError, +) + /** * Attempts to execute an API call safely, providing error handling. * @@ -40,12 +64,10 @@ value class ApiResponseRaise( suspend inline fun safeApiCall( crossinline call: suspend ApiResponseRaise.() -> T, crossinline onError: suspend (ApiResponseError) -> T, -): T { - return recover( - block = { call(ApiResponseRaise(raise = this)) }, - recover = { - Timber.w(it, "Unable to perform safe API call") - onError(it) - }, - ) -} \ No newline at end of file +): T = recover( + block = { call(ApiResponseRaise(raise = this)) }, + recover = { + Timber.w(it, "Unable to perform safe API call") + onError(it) + }, +) \ No newline at end of file diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 306603b7c2..4847e15ddd 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 9f6c1aa883..4bf2f7eb2a 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.feedback import android.content.Context import android.content.pm.PackageInfo import android.os.Build +import com.tangem.blockchain.common.Blockchain import com.tangem.data.feedback.converters.BlockchainInfoConverter import com.tangem.data.feedback.converters.CardInfoConverter import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -13,6 +14,9 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import timber.log.Timber import java.io.File import java.io.FileWriter @@ -35,6 +39,8 @@ internal class DefaultFeedbackRepository( private val context: Context, ) : FeedbackRepository { + private val blockchainsErrors = MutableStateFlow>(emptyMap()) + override suspend fun getUserWalletsInfo(): UserWalletsInfo { return UserWalletsInfo( selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, @@ -52,6 +58,16 @@ internal class DefaultFeedbackRepository( .map(BlockchainInfoConverter::convert) } + override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? { + return walletManagersStore + .getSyncOrNull( + userWalletId = getSelectedUserWallet().walletId, + blockchain = Blockchain.fromId(blockchainId), + derivationPath = derivationPath, + ) + ?.let(BlockchainInfoConverter::convert) + } + override fun getPhoneInfo(): PhoneInfo { return PhoneInfo( phoneModel = Build.MODEL, @@ -60,6 +76,22 @@ internal class DefaultFeedbackRepository( ) } + override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { + blockchainsErrors.update { + it.toMutableMap().apply { + put(getSelectedUserWallet().walletId, error) + } + } + } + + override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? { + return blockchainsErrors.value[getSelectedUserWallet().walletId].also { + if (it == null) { + Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}") + } + } + } + override suspend fun getAppLogs(): List { return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index ea61431f29..c4677126a6 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -6,7 +6,9 @@ import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import com.tangem.domain.tokens.model.CryptoCurrency +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import kotlinx.coroutines.yield import java.math.BigDecimal import java.net.URLDecoder @@ -14,15 +16,20 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { private data class QrScanningEvent(val type: SourceType, val qrCode: String) - private val scannedEvents = MutableSharedFlow() + private val scannedEvents = MutableSharedFlow(replay = 1) override suspend fun emitResult(type: SourceType, qrCode: String) { scannedEvents.emit(QrScanningEvent(type, qrCode)) } + @OptIn(ExperimentalCoroutinesApi::class) override fun subscribeToScanningResults(type: SourceType) = scannedEvents .filter { it.type == type } .map { it.qrCode } + .onEach { + yield() // if we have more than one sub, we must allow them to collect emitted value + scannedEvents.resetReplayCache() + } override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult { val withoutSchema = stripSchema(qrCode, cryptoCurrency) diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index f12116b4cf..96f4640242 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -15,8 +15,6 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.data.source.preferences) - implementation(projects.domain.balanceHiding.models) implementation(projects.domain.settings) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 71e8f8d654..335d4b86fd 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -1,23 +1,25 @@ package com.tangem.data.settings -import com.tangem.data.source.preferences.PreferencesDataSource 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.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import org.joda.time.DateTime internal class DefaultSettingsRepository( - private val preferencesDataSource: PreferencesDataSource, private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, ) : SettingsRepository { override suspend fun shouldShowSaveUserWalletScreen(): Boolean { - return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen } + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, + default = true, + ) + } + + override suspend fun setShouldShowSaveUserWalletScreen(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, value = value) } override suspend fun isWalletScrollPreviewEnabled(): Boolean { @@ -75,4 +77,38 @@ internal class DefaultSettingsRepository( value = isEnabled, ) } + + override suspend fun wasApplicationStopped(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_APPLICATION_STOPPED_KEY, default = false) + } + + override suspend fun setWasApplicationStopped(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.WAS_APPLICATION_STOPPED_KEY, value = value) + } + + override suspend fun shouldOpenWelcomeScreenOnResume(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY, + default = false, + ) + } + + override suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY, value = value) + } + + override suspend fun shouldSaveAccessCodes(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, default = false) + } + + override suspend fun setShouldSaveAccessCodes(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, value = value) + } + + override suspend fun incrementAppLaunchCounter() { + appPreferencesStore.editData { preferences -> + val count = preferences.getOrDefault(key = PreferencesKeys.APP_LAUNCH_COUNT_KEY, default = 0) + preferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY] = count + 1 + } + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 880f0d36b1..4f6bf8e17b 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -3,12 +3,10 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.settings.DefaultPromoSettingsRepository -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,16 +19,8 @@ internal object SettingsDataModule { @Provides @Singleton - fun provideSettingsRepository( - preferencesDataSource: PreferencesDataSource, - appPreferencesStore: AppPreferencesStore, - dispatchers: CoroutineDispatcherProvider, - ): SettingsRepository { - return DefaultSettingsRepository( - preferencesDataSource = preferencesDataSource, - appPreferencesStore = appPreferencesStore, - dispatchers = dispatchers, - ) + fun provideSettingsRepository(appPreferencesStore: AppPreferencesStore): SettingsRepository { + return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore) } @Provides diff --git a/data/source/preferences/build.gradle.kts b/data/source/preferences/build.gradle.kts deleted file mode 100644 index 3768a33a8c..0000000000 --- a/data/source/preferences/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.android.library) - id("configuration") -} - -android { - namespace = "com.tangem.data.source.preferences" -} - -dependencies { - implementation(deps.androidx.core.ktx) - implementation(deps.moshi) - implementation(deps.moshi.kotlin) - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - - // For MoshiJsonConverter - implementation(deps.tangem.card.core) -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt deleted file mode 100644 index e12d679fd7..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.data.source.preferences - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import javax.inject.Inject - -// 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories -// TODO: Replace shared preferences with DataStore -@Deprecated("Create repository instead") -class PreferencesDataSource @Inject internal constructor(applicationContext: Context) { - - private val preferences: SharedPreferences = - applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - - init { - incrementLaunchCounter() - } - - var shouldShowSaveUserWalletScreen: Boolean - get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true) - set(value) = preferences.edit { - putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value) - } - - var shouldSaveAccessCodes: Boolean - get() = preferences.getBoolean(SAVE_ACCESS_CODES_KEY, false) - set(value) = preferences.edit { - putBoolean(SAVE_ACCESS_CODES_KEY, value) - } - - var wasApplicationStopped: Boolean - get() = preferences.getBoolean(APPLICATION_STOPPED_KEY, false) - set(value) = preferences.edit { - putBoolean(APPLICATION_STOPPED_KEY, value) - } - - var shouldOpenWelcomeScreenOnResume: Boolean - get() = preferences.getBoolean(OPEN_WELCOME_ON_RESUME_KEY, false) - set(value) = preferences.edit { - putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value) - } - - private fun incrementLaunchCounter() { - var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0) - preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) } - } - - companion object { - private const val PREFERENCES_NAME = "tapPrefs" - private const val APP_LAUNCH_COUNT_KEY = "launchCount" - private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown" - private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes" - private const val APPLICATION_STOPPED_KEY = "applicationStopped" - private const val OPEN_WELCOME_ON_RESUME_KEY = "openWelcomeOnResume" - } -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/adapters/BigDecimalAdapter.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/adapters/BigDecimalAdapter.kt deleted file mode 100644 index d2adc38e1d..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/adapters/BigDecimalAdapter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.data.source.preferences.adapters - -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson -import java.math.BigDecimal - -class BigDecimalAdapter { - @FromJson - fun fromJson(value: String) = BigDecimal(value) - - @ToJson - fun toJson(value: BigDecimal) = value.toString() -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/di/PreferencesStoreModule.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/di/PreferencesStoreModule.kt deleted file mode 100644 index 5961efde84..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/di/PreferencesStoreModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.data.source.preferences.di - -import android.content.Context -import com.tangem.data.source.preferences.PreferencesDataSource -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object PreferencesStoreModule { - - @Provides - @Singleton - fun providePreferencesStore(@ApplicationContext context: Context) = PreferencesDataSource(context) -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/Migration.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/Migration.kt deleted file mode 100644 index e1cc25e4ba..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/Migration.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.data.source.preferences.storage - -internal interface Migration { - fun migrate() -} \ No newline at end of file diff --git a/data/staking/.gitignore b/data/staking/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/staking/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts new file mode 100644 index 0000000000..83a8cccc87 --- /dev/null +++ b/data/staking/build.gradle.kts @@ -0,0 +1,36 @@ +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.data.staking" +} + +dependencies { + + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.domain.staking) + implementation(projects.features.staking.api) + + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Others dependencies + implementation(deps.jodatime) + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + // endregion +} 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 new file mode 100644 index 0000000000..a60a8c5141 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -0,0 +1,68 @@ +package com.tangem.data.staking + +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultStakingRepository( + private val stakeKitApi: StakeKitApi, + private val stakingFeatureToggles: StakingFeatureToggles, + private val dispatchers: CoroutineDispatcherProvider, +) : StakingRepository { + + override fun getStakingAvailability(blockchainId: String): StakingAvailability { + if (!stakingFeatureToggles.isStakingEnabled) { + return StakingAvailability.Unavailable + } + + return integrationIdMap[Blockchain.fromId(blockchainId)]?.let { + StakingAvailability.Available(it) + } ?: StakingAvailability.Unavailable + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { + return withContext(dispatchers.io) { + val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() + + StakingEntryInfo( + interestRate = yield.apy, + periodInDays = yield.metadata.cooldownPeriod.days, + tokenSymbol = yield.token.symbol, + ) + } + } + + companion object { + private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" + private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" + private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" + private const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-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 BINANCE_INTEGRATION_ID = "binance-bnb-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 val integrationIdMap = mapOf( + Blockchain.Solana to SOLANA_INTEGRATION_ID, + Blockchain.Cosmos to COSMOS_INTEGRATION_ID, + Blockchain.Polkadot to POLKADOT_INTEGRATION_ID, + Blockchain.Polygon to ETHEREUM_INTEGRATION_ID, + Blockchain.Avalanche to AVALANCHE_INTEGRATION_ID, + Blockchain.Tron to TRON_INTEGRATION_ID, + Blockchain.Cronos to CRONOS_INTEGRATION_ID, + Blockchain.Binance to BINANCE_INTEGRATION_ID, + Blockchain.Kava to KAVA_INTEGRATION_ID, + Blockchain.Near to NEAR_INTEGRATION_ID, + Blockchain.Tezos to TEZOS_INTEGRATION_ID, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt new file mode 100644 index 0000000000..0f5076bb01 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -0,0 +1,31 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.DefaultStakingRepository +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +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 StakingDataModule { + + @Provides + @Singleton + fun provideStakingRepository( + stakeKitApi: StakeKitApi, + stakingFeatureToggles: StakingFeatureToggles, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): StakingRepository { + return DefaultStakingRepository( + stakeKitApi = stakeKitApi, + stakingFeatureToggles = stakingFeatureToggles, + dispatchers = coroutineDispatcherProvider, + ) + } +} \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index f183caa14f..7585dedc01 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { /** Project - Utils */ implementation(projects.core.utils) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) /** Tangem SDKs */ implementation(deps.tangem.blockchain) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt index b6d7d5395e..17e39d7f0d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt @@ -1,9 +1,9 @@ package com.tangem.data.tokens.paging import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.getNetworkStandardType import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Token import com.tangem.utils.converter.Converter 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 6074c94609..89bb9aca17 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 @@ -1,6 +1,9 @@ package com.tangem.data.tokens.repository +import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.* @@ -15,11 +18,11 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.AssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.core.error.DataError +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -55,6 +58,10 @@ internal class DefaultCurrenciesRepository( private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers) + private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( + value = emptyMap(), + ) + override suspend fun saveTokens( userWalletId: UserWalletId, currencies: List, @@ -166,6 +173,23 @@ internal class DefaultCurrenciesRepository( } } + override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> { + return lceFlow { + val userWallet = catch({ getUserWallet(userWalletId) }) { + raise(it) + } + + if (userWallet.isMultiCurrency) { + getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId).collect(::send) + } else { + val currency = catch({ getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) { + raise(it) + } + send(listOf(currency)) + } + } + } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) @@ -215,10 +239,34 @@ internal class DefaultCurrenciesRepository( .cancellable() } + override fun getMultiCurrencyWalletCurrenciesUpdatesLce( + userWalletId: UserWalletId, + ): LceFlow> = lceFlow { + val userWallet = getUserWallet(userWalletId) + catch({ ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) }) { + raise(it) + } + + launch(dispatchers.io) { + combine( + getMultiCurrencyWalletCurrencies(userWallet).distinctUntilChanged(), + isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } }, + ) { currencies, isFetching -> + send(currencies, isStillLoading = isFetching) + }.collect() + } + + withContext(dispatchers.io) { + catch({ fetchTokensIfCacheExpired(userWallet, refresh = false) }) { + raise(it) + } + } + } + override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, - ): List { + ): List = withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) @@ -228,7 +276,7 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - return responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) + responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) } override suspend fun getMultiCurrencyWalletCurrency( @@ -356,9 +404,17 @@ internal class DefaultCurrenciesRepository( balance = balance, ) } + is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency) } } + override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { + return CryptoCurrencyFactory().createToken( + cryptoCurrency = cryptoCurrency, + network = network, + ) + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return userTokensStore.get(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( @@ -369,11 +425,21 @@ internal class DefaultCurrenciesRepository( } private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { - cacheRegistry.invokeOnExpire( - key = getTokensCacheKey(userWallet.walletId), - skipCache = refresh, - block = { fetchTokens(userWallet) }, - ) + try { + isMultiCurrencyWalletCurrenciesFetching.update { + it + (userWallet.walletId to true) + } + + cacheRegistry.invokeOnExpire( + key = getTokensCacheKey(userWallet.walletId), + skipCache = refresh, + block = { fetchTokens(userWallet) }, + ) + } finally { + isMultiCurrencyWalletCurrenciesFetching.update { + it - userWallet.walletId + } + } } private suspend fun fetchTokens(userWallet: UserWallet) { 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 8f21f5d495..ea56a8adbf 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 @@ -1,9 +1,11 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider +import com.tangem.blockchain.common.FeeResourceAmountProvider import com.tangem.blockchain.common.ReserveAmountProvider import com.tangem.blockchain.common.UtxoAmountLimitProvider import com.tangem.data.tokens.converters.UtxoConverter +import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.repository.CurrencyChecksRepository @@ -41,6 +43,40 @@ internal class DefaultCurrencyChecksRepository( return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null } + override suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? { + val manager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + + return if (manager is FeeResourceAmountProvider) { + val feeResource = manager.getFeeResource() + CurrencyAmount( + value = feeResource.value, + maxValue = feeResource.maxValue, + ) + } else { + null + } + } + + override suspend fun checkIfFeeResourceEnough( + amount: BigDecimal, + userWalletId: UserWalletId, + network: Network, + ): Boolean { + val manager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + + return if (manager is FeeResourceAmountProvider) { + manager.isFeeEnough(amount) + } else { + false + } + } + override suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean { val manager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt index 006ca60bb9..02fdef2739 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.card.EllipticCurve import com.tangem.data.tokens.utils.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -63,7 +64,7 @@ internal class DefaultNetworksCompatibilityRepository( @Throws(IllegalArgumentException::class) override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List { val scanResponse = getWalletOrThrow(userWalletId).scanResponse - return Blockchain.values() + return Blockchain.entries .filter { blockchain -> scanResponse.card.supportedBlockchains(scanResponse.cardTypesResolver).contains(blockchain) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index fe6ab4046f..c4a12be730 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,7 +1,9 @@ package com.tangem.data.tokens.repository +import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory @@ -9,8 +11,9 @@ import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -22,10 +25,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.* import timber.log.Timber @Suppress("LongParameterList") @@ -43,6 +43,10 @@ internal class DefaultNetworksRepository( private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() } private val networkStatusFactory by lazy { NetworkStatusFactory() } + private val isNetworkStatusesFetching = MutableStateFlow( + value = emptyMap(), + ) + override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, @@ -58,6 +62,26 @@ internal class DefaultNetworksRepository( } .cancellable() + override fun getNetworkStatusesUpdatesLce( + userWalletId: UserWalletId, + networks: Set, + ): LceFlow> = lceFlow { + launch(dispatchers.io) { + combine( + networksStatusesStore.get(userWalletId), + isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } }, + ) { statuses, isFetching -> + send(statuses, isStillLoading = isFetching) + }.collect() + } + + withContext(dispatchers.io) { + catch({ fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) }) { + raise(it) + } + } + } + override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { val currencies = getCurrencies(userWalletId, networks) withContext(dispatchers.io) { @@ -82,15 +106,15 @@ internal class DefaultNetworksRepository( override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network, - ): List { + ): List = withContext(dispatchers.io) { // Get list of currencies matching [network] val currencies = getCurrencies(userWalletId) .filter { currency -> network.id == currency.network.id } // There is no currencies matching given [networks] in [userWalletId] - if (currencies.toList().isEmpty()) return emptyList() + if (currencies.toList().isEmpty()) return@withContext emptyList() - return currencies.toList().map { currency -> + currencies.toList().map { currency -> CryptoCurrencyAddress( cryptoCurrency = currency, address = walletManagersFacade.getAddresses(userWalletId, currency.network) @@ -105,15 +129,25 @@ internal class DefaultNetworksRepository( networks: Set, refresh: Boolean, ) { - val currencies = getCurrencies(userWalletId, networks) - coroutineScope { - networks - .map { network -> - async { - fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh) + try { + isNetworkStatusesFetching.update { + it + (userWalletId to true) + } + + val currencies = getCurrencies(userWalletId, networks) + coroutineScope { + networks + .map { network -> + async { + fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh) + } } - } - .awaitAll() + .awaitAll() + } + } finally { + isNetworkStatusesFetching.update { + it - userWalletId + } } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index d362189832..1dee0062ce 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -1,6 +1,6 @@ package com.tangem.data.tokens.repository -import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.api.safeApiCallWithTimeout import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.QuotesConverter import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter @@ -68,11 +68,10 @@ internal class DefaultQuotesRepository( } } - override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote { + override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? { return withContext(dispatchers.io) { val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull() - requireNotNull(quote) { "Unable to get quote for $currencyId" } - quotesConverter.convert(quote) + quote?.let { quotesConverter.convert(it) } } } @@ -92,30 +91,30 @@ internal class DefaultQuotesRepository( if (expiredCurrenciesIds.isEmpty()) return quotesFetchedForAppCurrency = appCurrencyId + fetchQuotes(expiredCurrenciesIds, appCurrencyId) } } private suspend fun fetchQuotes(rawCurrenciesIds: Set, appCurrencyId: String) { val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds) - val response = safeApiCall( + val response = safeApiCallWithTimeout( call = { val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() }, - onError = { + onError = { error -> cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey)) - null + + throw error }, ) - if (response != null) { - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( - response, - replacementIdsResult.idsFiltered, - ) - quotesStore.store(updatedResponse) - } + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response, + replacementIdsResult.idsFiltered, + ) + quotesStore.store(updatedResponse) } private suspend fun filterExpiredCurrenciesIds( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt index e083c266df..a10187713a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt @@ -4,11 +4,11 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.paging.CoinsPagingSource import com.tangem.data.tokens.utils.FoundTokenConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.FoundToken import com.tangem.domain.tokens.model.Token import com.tangem.domain.tokens.repository.QuotesRepository diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index 9d9e0f3cbc..55b2b0b161 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -1,10 +1,11 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken @@ -90,6 +91,28 @@ class CryptoCurrencyFactory { ) } + fun createToken(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { + val sdkToken = SdkToken( + name = cryptoCurrency.name, + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + id = cryptoCurrency.id.rawCurrencyId, + ) + val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.id.value) ?: Blockchain.Unknown + val id = getTokenId(network, sdkToken) + return CryptoCurrency.Token( + id = id, + network = network, + name = sdkToken.name, + symbol = sdkToken.symbol, + iconUrl = getTokenIconUrl(blockchain, sdkToken), + decimals = sdkToken.decimals, + isCustom = isCustomToken(id, network), + contractAddress = sdkToken.contractAddress, + ) + } + data class Token( val name: String, val symbol: String, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index 8b2a7175c6..251a2b420f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -1,8 +1,9 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.FeePaidCurrency +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.Network import timber.log.Timber @@ -28,6 +29,7 @@ internal fun getNetwork( derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider), currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), + hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 5f01503b2f..81905be776 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -81,8 +81,7 @@ internal class NetworkStatusFactory { } is CryptoCurrency.Token -> transactions.filterTo(hashSetOf()) { transaction -> transaction is CryptoCurrencyTransaction.Token && - transaction.tokenId == currency.id.rawCurrencyId && - transaction.tokenContractAddress == currency.contractAddress + transaction.tokenContractAddress.equals(currency.contractAddress, ignoreCase = true) } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index 0e69a31330..819dbe2a39 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -52,7 +52,7 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { "optimistic-ethereum" to "ethereum", "arbitrum-one" to "ethereum", "zksync-ethereum" to "ethereum", - "manta-network-ethereum" to "ethereum", + "manta-pacific" to "ethereum", "polygon-zkevm-ethereum" to "ethereum", "aurora-ethereum" to "ethereum", "base-ethereum" to "ethereum", diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 5435cebfd9..97c5d2ed4d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -2,10 +2,10 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse @@ -87,11 +87,24 @@ class ResponseCryptoCurrenciesFactory { private fun Blockchain.getNameForCoin(responseToken: UserTokensResponse.Token): String { return when (this) { - // workaround: Dischain was renamed but backend still returns the old name, + // workaround: for Blockchains full name different than backend name, // get name and symbol from enum Blockchain until backend renamed // [REDACTED_JIRA] Blockchain.Dischain, Blockchain.Arbitrum, + Blockchain.ArbitrumTestnet, + Blockchain.Aurora, + Blockchain.AuroraTestnet, + Blockchain.Manta, + Blockchain.MantaTestnet, + Blockchain.ZkSyncEra, + Blockchain.ZkSyncEraTestnet, + Blockchain.PolygonZkEVM, + Blockchain.PolygonZkEVMTestnet, + Blockchain.Base, + Blockchain.BaseTestnet, + Blockchain.Telos, + Blockchain.Cronos, -> this.fullName else -> responseToken.name } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 54f468860b..a944703601 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -2,8 +2,8 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency.ID import com.tangem.domain.tokens.model.Network diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt index db1650880f..ec40d6e52c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt @@ -1,9 +1,9 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toCoinId /** * Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index 7e75db2896..7e8372da6c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -1,7 +1,7 @@ package com.tangem.data.tokens.utils +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency class UserTokensResponseFactory { diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index e1b448fb10..615758dcf6 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -16,15 +16,19 @@ dependencies { implementation(deps.tangem.blockchain) /** Core */ + implementation(projects.core.datasource) implementation(projects.core.utils) /** Domain */ implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + implementation(deps.timber) } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 3fe078eaad..d36cb4f74b 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -4,22 +4,26 @@ import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber +import java.math.BigDecimal internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, + private val walletManagersStore: WalletManagersStore, private val coroutineDispatcherProvider: CoroutineDispatcherProvider, ) : TransactionRepository { @@ -30,6 +34,9 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, + isSwap: Boolean, + txExtras: TransactionExtras?, + hash: String?, ): TransactionData? = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( @@ -38,11 +45,57 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) - return@withContext walletManager?.createTransaction(amount, fee, destination)?.copy( - extras = getMemoExtras(network.id.value, memo), + return@withContext walletManager?.createTransactionInternal( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + network = network, + isSwap = isSwap, + txExtras = txExtras, + hash = hash, ) } + override suspend fun validateTransaction( + amount: Amount, + fee: Fee?, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean, + txExtras: TransactionExtras?, + hash: String?, + ): Result = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + val validator = walletManager as? TransactionValidator + + if (validator != null) { + val transaction = walletManager.createTransactionInternal( + amount = amount, + fee = fee ?: Fee.Common(amount = amount), + memo = memo, + destination = destination, + network = network, + isSwap = isSwap, + txExtras = txExtras, + hash = hash, + ) + + validator.validate(transaction = transaction) + } else { + Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") + Result.success(Unit) + } + } + override suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, @@ -58,6 +111,35 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + @Suppress("LongParameterList") + private fun WalletManager.createTransactionInternal( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + network: Network, + isSwap: Boolean, + txExtras: TransactionExtras?, + hash: String?, + ): TransactionData { + // TODO: refactor workaround to use general mechanism in bsdk for build tx for DEX + val txAmount = if (isSwap) { + createAmountForSwap(amount) + } else { + amount + } + + if (txExtras != null && memo != null) { + // throw error for now to avoid programmers errors when use extras + error("Both txExtras and memo provided, use only one of them") + } + val extras = txExtras ?: getMemoExtras(network.id.value, memo) + return createTransaction(txAmount, fee, destination).copy( + hash = hash, + extras = extras, + ) + } + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) if (memo == null) return null @@ -76,9 +158,24 @@ internal class DefaultTransactionRepository( Blockchain.TerraV2, -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) + Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) else -> null } } + + private fun createAmountForSwap(amount: Amount): Amount { + return when (amount.type) { + is AmountType.Coin -> amount + else -> { + // 1. when creates swap amount for NonNativeToken, amount should be ZERO + // 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress + Amount( + currencySymbol = amount.currencySymbol, + value = BigDecimal.ZERO, + decimals = amount.decimals, + ) + } + } + } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt similarity index 77% rename from data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt rename to data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 6831cc8ae4..4d0bd6ac5e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -1,35 +1,39 @@ -package com.tangem.data.wallets +package com.tangem.data.transaction import android.net.Uri import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.errors.ParsedQrCodeErrors -import com.tangem.domain.wallets.repository.WalletAddressServiceRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext import java.math.BigInteger class DefaultWalletAddressServiceRepository( private val walletManagersFacade: WalletManagersFacade, + private val dispatchers: CoroutineDispatcherProvider, ) : WalletAddressServiceRepository { - override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean { - val blockchain = Blockchain.fromId(network.id.value) + override suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean = + withContext(dispatchers.io) { + val blockchain = Blockchain.fromId(network.id.value) - return if (blockchain.isNear()) { - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, - ) ?: return false - (walletManager as? NearWalletManager)?.validateAddress(address) ?: false - } else { - blockchain.validateAddress(address) + if (blockchain.isNear()) { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) ?: return@withContext false + (walletManager as? NearWalletManager)?.validateAddress(address) ?: false + } else { + blockchain.validateAddress(address) + } } - } override fun validateMemo(network: Network, memo: String): Boolean { if (memo.isEmpty()) return true diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index c8949d39c0..d5fffe95ed 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -2,8 +2,11 @@ package com.tangem.data.transaction.di import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.data.transaction.DefaultWalletAddressServiceRepository +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -20,10 +23,12 @@ internal object TransactionDataModule { @Singleton fun providesTransactionRepository( walletManagersFacade: WalletManagersFacade, + walletManagersStore: WalletManagersStore, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): TransactionRepository { return DefaultTransactionRepository( walletManagersFacade = walletManagersFacade, + walletManagersStore = walletManagersStore, coroutineDispatcherProvider = coroutineDispatcherProvider, ) } @@ -33,4 +38,16 @@ internal object TransactionDataModule { fun providesFeeRepository(): FeeRepository { return DefaultFeeRepository() } + + @Provides + @Singleton + fun providesWalletAddressServiceRepository( + walletManagersFacade: WalletManagersFacade, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): WalletAddressServiceRepository { + return DefaultWalletAddressServiceRepository( + walletManagersFacade = walletManagersFacade, + dispatchers = coroutineDispatcherProvider, + ) + } } \ No newline at end of file diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 2b5b2f8165..3a8e769a69 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index 27f3b8e7bb..ebc92a5036 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,10 +24,12 @@ internal object TxHistoryDataModule { walletManagersFacade: WalletManagersFacade, userWalletsStore: UserWalletsStore, txHistoryItemsStore: TxHistoryItemsStore, + dispatchers: CoroutineDispatcherProvider, ): TxHistoryRepository = DefaultTxHistoryRepository( cacheRegistry, walletManagersFacade, userWalletsStore, txHistoryItemsStore, + dispatchers, ) } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index eada356d1e..fb35baa42f 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -20,7 +20,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.utils.SdkPageConverter import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext import timber.log.Timber class DefaultTxHistoryRepository( @@ -28,6 +30,7 @@ class DefaultTxHistoryRepository( private val walletManagersFacade: WalletManagersFacade, private val userWalletsStore: UserWalletsStore, private val txHistoryItemsStore: TxHistoryItemsStore, + private val dispatchers: CoroutineDispatcherProvider, ) : TxHistoryRepository { private val sdkPageConverter by lazy { SdkPageConverter() } @@ -85,7 +88,7 @@ class DefaultTxHistoryRepository( network = network, ) val lastTxHash = walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty() - return when (val txExploreState = blockchain?.getExploreTxUrl(lastTxHash)) { + return when (val txExploreState = blockchain.getExploreTxUrl(lastTxHash)) { is TxExploreState.Url -> txExploreState.url else -> "" } @@ -96,8 +99,8 @@ class DefaultTxHistoryRepository( currency: CryptoCurrency, pageSize: Int, refresh: Boolean, - ): List { - return try { + ): List = withContext(dispatchers.io) { + try { cacheRegistry.invokeOnExpire( key = getTxHistoryPageKey(currency, userWalletId, Page.Initial), skipCache = refresh, diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ac55319dfe..e552eb58b0 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { /** Project - Utils */ implementation(projects.core.utils) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) /** Project - Libs */ debugImplementation(projects.libs.visa) diff --git a/data/wallet-connect/.gitignore b/data/wallet-connect/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/wallet-connect/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts new file mode 100644 index 0000000000..5dc33697df --- /dev/null +++ b/data/wallet-connect/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.android.library) + id("configuration") +} + +android { + namespace = "com.tangem.data.walletconnect" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.walletConnect) + implementation(projects.domain.wallets.models) + + /* Project - Data */ + implementation(projects.core.datasource) + + /* Project - Core */ + implementation(projects.core.utils) + + /* DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /* Other */ + implementation(deps.kotlin.coroutines) +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt new file mode 100644 index 0000000000..f701e44de9 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt @@ -0,0 +1,21 @@ +package com.tangem.data.walletconnect + +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.walletconnect.repository.WalletConnectRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultWalletConnectRepository( + private val userWalletsStore: UserWalletsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : WalletConnectRepository { + + override suspend fun checkIsAvailable(userWalletId: UserWalletId): Boolean = withContext(dispatchers.io) { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "User wallet with id $userWalletId not found" + } + + userWallet.isMultiCurrency + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt new file mode 100644 index 0000000000..02c6f391db --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -0,0 +1,25 @@ +package com.tangem.data.walletconnect.di + +import com.tangem.data.walletconnect.DefaultWalletConnectRepository +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.walletconnect.repository.WalletConnectRepository +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 WalletConnectDataModule { + + @Provides + @Singleton + fun providesWalletConnectRepository( + userWalletsStore: UserWalletsStore, + dispatchers: CoroutineDispatcherProvider, + ): WalletConnectRepository { + return DefaultWalletConnectRepository(userWalletsStore, dispatchers) + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletNamesMigrationRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletNamesMigrationRepository.kt new file mode 100644 index 0000000000..60d442028e --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletNamesMigrationRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.data.wallets + +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.datasource.local.preferences.utils.store +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository + +class DefaultWalletNamesMigrationRepository( + private val appPreferencesStore: AppPreferencesStore, +) : WalletNamesMigrationRepository { + + override suspend fun isMigrationDone(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.IS_WALLET_NAMES_MIGRATION_DONE_KEY, + default = false, + ) + } + + override suspend fun setMigrationDone() { + appPreferencesStore.store(PreferencesKeys.IS_WALLET_NAMES_MIGRATION_DONE_KEY, true) + } +} \ 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 26e9f82859..c41bb99652 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 @@ -1,10 +1,9 @@ package com.tangem.data.wallets.di -import com.tangem.data.wallets.DefaultWalletAddressServiceRepository +import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.repository.WalletAddressServiceRepository +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import dagger.Module import dagger.Provides @@ -24,9 +23,7 @@ internal object WalletsDataModule { @Provides @Singleton - fun providesWalletAddressServiceRepository( - walletManagersFacade: WalletManagersFacade, - ): WalletAddressServiceRepository { - return DefaultWalletAddressServiceRepository(walletManagersFacade) + fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository { + return DefaultWalletNamesMigrationRepository(appPreferencesStore) } } \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 4ec562467f..d317bafbf5 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.core) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) // TODO: Remove after new card scan result was implemented implementation(projects.domain.models) implementation(projects.domain.tokens.models) diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt deleted file mode 100644 index 538ac0f240..0000000000 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.domain.card - -import com.tangem.domain.card.repository.CardSdkConfigRepository - -/** - * Use case for getting access code saving status - * - * @property cardSdkConfigRepository repository for managing of CardSDK config - * -[REDACTED_AUTHOR] - */ -class GetAccessCodeSavingStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { - - operator fun invoke(): Boolean = cardSdkConfigRepository.isAccessCodeSavingEnabled() -} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt new file mode 100644 index 0000000000..7bdd4f2293 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.card + +import arrow.core.Either +import com.tangem.domain.card.models.ResetCardError +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for resetting card to factory settings + * +[REDACTED_AUTHOR] + */ +interface ResetCardUseCase { + + /** Reset card [card] to factory settings */ + suspend operator fun invoke(card: CardDTO): Either + + /** Reset backup card [cardNumber] with expected [UserWalletId] using [card] of reset card */ + suspend operator fun invoke( + cardNumber: Int, + card: CardDTO, + userWalletId: UserWalletId, + ): Either +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt new file mode 100644 index 0000000000..7db8690e89 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.card.models + +sealed interface ResetCardError { + + data object UserCanceled : ResetCardError + + data object AnotherSdkError : ResetCardError +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index 47d13a5a8c..98b2739bec 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -27,9 +27,6 @@ interface CardSdkConfigRepository { /** Update the card ID display format according to the [productType] of the scanned card */ fun updateCardIdDisplayFormat(productType: ProductType) - /** Check if access code saving is enabled */ - fun isAccessCodeSavingEnabled(): Boolean - /** Get common signer by [cardId] */ fun getCommonSigner(cardId: String?): CommonSigner diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt new file mode 100644 index 0000000000..25029586ad --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -0,0 +1,99 @@ +package com.tangem.domain.core.lce + +import arrow.core.identity +import com.tangem.domain.core.utils.flatMap +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading + +/** + * A sealed class representing the three states of a data load operation: Loading, Content, and Error. + * + * @param E The type of the error object. + * @param C The type of the content object. + */ +sealed class Lce { + + /** + * Represents the loading state, which may contain partial content. + * + * @param partialContent The partial content that has been loaded so far, if any. + */ + data class Loading(val partialContent: C?) : Lce() + + /** + * Represents the content state, which contains the loaded content. + * + * @param content The loaded content. + */ + data class Content(val content: C) : Lce() + + /** + * Represents the error state, which contains an error object. + * + * @param error The error that occurred during loading. + */ + data class Error(val error: E) : Lce() + + /** + * Applies the given functions to the content, error, or partial content of this Lce, depending on its state. + * + * @param ifLoading The function to apply if this is a [Loading] state. + * @param ifContent The function to apply if this is a [Content] state. + * @param ifError The function to apply if this is an [Error] state. + * @return The result of applying the corresponding function. + */ + inline fun fold( + ifLoading: (partialContent: C?) -> T, + ifContent: (content: C) -> T, + ifError: (error: E) -> T, + ): T = when (this) { + is Loading -> ifLoading(partialContent) + is Error -> ifError(error) + is Content -> ifContent(content) + } + + /** + * Transforms the content of this [Lce] by applying the given function. + * If this is a [Content] state, the function is applied to the [Content.content]. + * If this is a [Loading] state and partialContent is present, + * the function is applied to the [Loading.partialContent]. + * + * @param ifContent The function to apply to the content or partial content. + * @return A new [Lce] instance containing the result of applying the function. + */ + inline fun map(ifContent: (C) -> T): Lce = flatMap { content, isLoading -> + if (isLoading) { + lceLoading(ifContent(content)) + } else { + ifContent(content).lceContent() + } + } + + /** + * Transforms the error of this [Lce] by applying the given function, if this is an [Error] state. + * + * @param ifError The function to apply to the error. + * @return A new [Lce] with the transformed error, or this [Lce] unchanged if it is not an [Error] state. + */ + inline fun mapError(ifError: (E) -> T): Lce = when (this) { + is Loading -> this + is Content -> this + is Error -> ifError(error).lceError() + } + + /** + * Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading] + * and [isPartialContentAccepted] is `true`, `null` if it's a [Lce.Error]. + * + * @param isPartialContentAccepted A flag indicating whether partial content should be accepted + * and returned. Default is `true`. + * + * @return The content of this [Lce] or `null`. + */ + fun getOrNull(isPartialContentAccepted: Boolean = true): C? = fold( + ifLoading = { if (isPartialContentAccepted) it else null }, + ifContent = ::identity, + ifError = { null }, + ) +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt new file mode 100644 index 0000000000..9be30e9699 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -0,0 +1,104 @@ +package com.tangem.domain.core.lce + +import arrow.core.raise.Raise +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kotlin.experimental.ExperimentalTypeInference + +/** + * A [Flow] of [Lce] + * + * @param E The type of the error object. + * @param C The type of the content object. + * */ +typealias LceFlow = Flow> + +/** + * A class that wraps a [LceRaise] instance for [Lce] type within a [ProducerScope]. + * It provides methods to handle [Lce] instances and raise errors within a [Flow]. + * + * @property raise The [LceRaise] instance that this class wraps. + * @property scope The [ProducerScope] that this class operates within. + * @property ifLoading The function to call if a loading state is raised. + */ +class LceFlowScope @PublishedApi internal constructor( + private val raise: LceRaise, + private val scope: ProducerScope>, + private val ifLoading: suspend LceFlowScope.(C?) -> Unit, +) : Raise, CoroutineScope by scope { + + /** + * Raises an [Lce] instance within the [ProducerScope]. + * It closes the [ProducerScope] after raise. + * + * @param r The [Lce] instance to raise. + */ + override fun raise(r: E): Nothing { + scope.launch(NonCancellable) { + scope.send(r.lceError()) + scope.close() + } + + raise.raise(r.lceError()) + } + + /** + * Sends a content value within the [ProducerScope]. + * If the content is still loading, it calls [ifLoading] lambda to retrieve a state. + * Otherwise, it wraps the content in a [Lce.Content] state. + * + * @param content The content value to send. + * @param isStillLoading A flag indicating whether the content is still loading. + */ + suspend fun send(content: C, isStillLoading: Boolean = false) { + val value = if (isStillLoading) { + ifLoading(content) + return + } else { + content.lceContent() + } + + scope.send(value) + } + + suspend fun send(value: Lce) { + scope.send(value) + } +} + +/** + * Creates a [LceFlow] by executing the given [block] within a [LceFlowScope] context. + * + * Flow starts with a [Lce.Loading] state. + * + * @param ifLoading The function to call if received a loading content. + * By default, it creates a new [Lce.Loading] state with the value returned by the [block]. + * @param block The block to execute within a [LceFlowScope] context. + * @return A [LceFlow] representing the result of the [block]. + */ +@OptIn(ExperimentalTypeInference::class) +fun lceFlow( + ifLoading: suspend LceFlowScope.(C?) -> Unit = { send(lceLoading(partialContent = it)) }, + @BuilderInference block: suspend LceFlowScope.() -> Unit, +): LceFlow { + return channelFlow { + trySend(lceLoading()) + + lce { + val scope = LceFlowScope( + raise = this@lce, + scope = this@channelFlow, + ifLoading = ifLoading, + ) + + block(scope) + } + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt new file mode 100644 index 0000000000..90dbc6d172 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.core.lce + +import arrow.atomic.Atomic +import arrow.core.raise.Raise +import arrow.core.raise.recover +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceLoading +import kotlin.experimental.ExperimentalTypeInference + +/** + * A class that wraps a [Raise] instance for [Lce] type. + * It provides methods to handle [Lce] instances and raise errors. + * + * @property raise The [Raise] instance that this class wraps. + * @property isLoading An [Atomic] boolean flag indicating whether a loading operation is in progress. + */ +class LceRaise @PublishedApi internal constructor( + private val raise: Raise>, +) : Raise> by raise { + + val isLoading: Atomic = Atomic(false) + + /** + * Binds the content of this [Lce] instance and handles its state. + * If this is a [Lce.Loading] state, sets the [isLoading] flag to true and calls the [ifLoading] function. + * If this is a [Lce.Content] state, returns the content. + * If this is a [Lce.Error] state, raises the error. + * + * @param ifLoading The function to call if this is a [Lce.Loading] state. + * By default, it raises a new [Lce.Loading] state. + * @return The content of this [Lce] instance. + */ + fun Lce.bind(): C = when (this) { + is Lce.Loading -> { + isLoading.set(true) + + raise(lceLoading()) + } + is Lce.Content -> content + is Lce.Error -> raise(r = this) + } + + /** + * Binds the content of this [Lce] instance and handles its state. + * If this is a [Lce.Loading] state, sets the [isLoading] flag to true and returns the partial content. + * If this is a [Lce.Content] state, returns the content. + * If this is a [Lce.Error] state, raises the error. + * + * @return The content of this [Lce] instance. + */ + fun Lce.bindOrNull(): C? = when (this) { + is Lce.Loading -> { + isLoading.set(true) + + partialContent + } + is Lce.Content -> content + is Lce.Error -> raise(r = this) + } +} + +/** + * Creates a [Lce] instance by executing the given [block] within a [LceRaise] context. + * + * @param ifLoading The function to call if the [block] raises a [Lce.Loading] state. + * By default, it creates a new [Lce.Loading] state with the value returned by the [block]. + * @param block The block to execute within a [LceRaise] context. + * @return A [Lce] instance representing the result of the [block]. + */ +@OptIn(ExperimentalTypeInference::class) +inline fun lce( + ifLoading: LceRaise.(C) -> Lce = { lceLoading(partialContent = it) }, + @BuilderInference block: LceRaise.() -> C, +): Lce = recover( + block = { + val raise = LceRaise(raise = this) + val value = block(raise) + + if (raise.isLoading.get()) { + ifLoading(raise, value) + } else { + value.lceContent() + } + }, + recover = { e: Lce -> e }, +) \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt new file mode 100644 index 0000000000..457f3e4f5b --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.core.utils + +import arrow.core.Either +import com.tangem.domain.core.lce.Lce +import kotlinx.coroutines.flow.Flow + +/** + * [Flow] of [Either] + * + * @param E type of left value + * @param A type of right value + * */ +typealias EitherFlow = Flow> + +/** + * Converts an [Either] instance to a [Lce] instance. + * If this is a [Either.Left], it is converted to a [Lce.Error] with the same error. + * If this is a [Either.Right], it is converted to a [Lce.Content] or [Lce.Loading] with the same content, + * depending on the [isStillLoading] parameter. + * + * @param isStillLoading A flag indicating whether the content is still loading. + * If true, the [Either.Right] is converted to a [Lce.Loading]. + * @return A [Lce] instance containing the same content or error as this [Either], + * and possibly indicating a loading state. + */ +inline fun Either.toLce(isStillLoading: Boolean = false): Lce { + return when (this) { + is Either.Left -> Lce.Error(value) + is Either.Right -> { + if (isStillLoading) { + Lce.Loading(value) + } else { + Lce.Content(value) + } + } + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/utils/LceExt.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/LceExt.kt new file mode 100644 index 0000000000..14f4d80e08 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/LceExt.kt @@ -0,0 +1,75 @@ +package com.tangem.domain.core.utils + +import arrow.core.Either +import arrow.core.identity +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.lce.Lce + +/** + * Creates a [Lce.Loading] instance with optional partial content. + * + * @param partialContent The partial content that has been loaded so far, if any. + * @return A [Lce.Loading] instance. + */ +fun lceLoading(partialContent: C? = null): Lce = Lce.Loading(partialContent) + +/** + * Wraps the receiver object in a [Lce.Content] instance. + * + * @return A [Lce.Content] instance containing the receiver object. + */ +fun C.lceContent(): Lce = Lce.Content(content = this) + +/** + * Wraps the receiver object in a [Lce.Error] instance. + * + * @return A [Lce.Error] instance containing the receiver object. + */ +fun E.lceError(): Lce = Lce.Error(error = this) + +/** + * Transforms this [Lce] instance by applying the given function into a new [Lce] instance. + * + * @param block The function to apply to the content of this [Lce]. + * @return A new [Lce] instance containing the result of applying the function. + * */ +inline fun Lce.flatMap( + block: (content: C, isLoading: Boolean) -> Lce, +): Lce = when (this) { + is Lce.Loading -> partialContent?.let { block(it, true) } ?: lceLoading() + is Lce.Content -> block(content, false) + is Lce.Error -> this +} + +/** + * Returns the content of this [Lce] if it's a [Lce.Content] or applies the given functions if it's a [Lce.Loading] or [Lce.Error]. + * + * @param ifLoading The function to apply if this is a [Lce.Loading] state. + * @param ifError The function to apply if this is a [Lce.Error] state. + * @return The content of this [Lce] or the result of applying the corresponding function. + */ +inline fun Lce.getOrElse(ifLoading: (maybeContent: C?) -> C, ifError: (error: E) -> C): C { + return fold( + ifLoading = ifLoading, + ifContent = ::identity, + ifError = ifError, + ) +} + +/** + * Transforms this [Lce] into an [Either] instance. + * If this is a [Lce.Content], the content is wrapped in a [Either.Right]. + * If this is a [Lce.Error], the error is wrapped in a [Either.Left]. + * If this is a [Lce.Loading], the [ifLoading] function is applied to the partial content and the result is wrapped in a + * [Either.Right]. + * + * @param ifLoading The function to apply if this is a [Lce.Loading] state. + * @return An [Either] instance containing the content or error of this [Lce], + * or the result of applying the [ifLoading] function to the partial content. + */ +inline fun Lce.toEither(ifLoading: (maybeContent: C?) -> C): Either = fold( + ifLoading = { ifLoading(it).right() }, + ifContent = { it.right() }, + ifError = { it.left() }, +) \ No newline at end of file diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt index e98373f775..130141ab85 100644 --- a/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoTransactionSender.kt @@ -3,8 +3,8 @@ package com.tangem.domain.demo import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result -import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult import kotlin.random.Random @@ -25,14 +25,17 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact return getFee(amount, walletManager.wallet.address) } - override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + override suspend fun send( + transactionData: TransactionData, + signer: TransactionSigner, + ): Result { val signerResponse = signer.sign( hash = getDataToSign(), publicKey = walletManager.wallet.publicKey, ) return when (signerResponse) { - is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) - is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) + is CompletionResult.Success -> Result.Failure(Exception(ID).toBlockchainSdkError()) + is CompletionResult.Failure -> Result.fromTangemSdkError(signerResponse.error) } } diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 3e7859b754..6a21560035 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(deps.jodatime) implementation(projects.core.res) + implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 5350902e33..a671bad033 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -1,9 +1,6 @@ package com.tangem.domain.feedback -import com.tangem.domain.feedback.models.BlockchainInfo -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.models.PhoneInfo -import com.tangem.domain.feedback.models.UserWalletsInfo +import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.utils.breakLine import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses @@ -68,6 +65,24 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("App version", phoneInfo.appVersion) } + fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) { + builder.appendKeyValue("Blockchain", info.blockchain) + builder.appendKeyValue("Derivation path", info.derivationPath) + builder.appendKeyValue("Host", info.host) + builder.appendKeyValue("Token", error.tokenSymbol) + builder.appendKeyValue("Error", error.errorMessage) + + builder.appendDelimiter() + + builder.appendAddresses( + key = "Source address${info.addresses.isMultiple(suffix = "es")}", + addresses = info.addresses, + ) + builder.appendKeyValue("Destination address", error.destinationAddress) + builder.appendKeyValue("Amount", error.amount) + builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt new file mode 100644 index 0000000000..50998a4ed8 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.feedback + +import android.content.res.Resources +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmail +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.feedback.utils.* + +/** + * Get email with feedback for support + * + * @property feedbackRepository feedback repository + * @property resources resources for getting strings + * +[REDACTED_AUTHOR] + */ +class GetFeedbackEmailUseCase( + private val feedbackRepository: FeedbackRepository, + private val resources: Resources, +) { + + private val emailSubjectResolver = EmailSubjectResolver(resources) + private val emailMessageTitleResolver = EmailMessageTitleResolver(resources) + private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) + + suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { + val cardInfo = feedbackRepository.getCardInfo() + + val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs()) + + return FeedbackEmail( + address = getAddress(cardInfo), + subject = emailSubjectResolver.resolve(type, cardInfo), + message = createMessage(type, cardInfo), + file = feedbackRepository.createLogFile(logs = formattedLogs), + ) + } + + private fun getAddress(cardInfo: CardInfo): String { + return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + } + + private suspend fun createMessage(type: FeedbackEmailType, cardInfo: CardInfo): String { + return StringBuilder().apply { + val title = emailMessageTitleResolver.resolve(type) + append(title) + + skipLine() + + appendDisclaimerIfNeeded(type) + + skipLine() + + val body = emailMessageBodyResolver.resolve(type, cardInfo) + append(body) + }.toString() + } + + private fun StringBuilder.appendDisclaimerIfNeeded(type: FeedbackEmailType): StringBuilder { + return if (type is FeedbackEmailType.ScanningProblem) { + this + } else { + append(resources.getString(R.string.feedback_data_collection_message)) + } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt deleted file mode 100644 index 6ab491c734..0000000000 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.domain.feedback - -import android.content.res.Resources -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.models.SupportFeedbackEmail -import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.feedback.utils.skipLine -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormatterBuilder -import java.util.Locale - -/** - * Get email with feedback for support - * - * @property feedbackRepository feedback repository - * @property resources resources for getting strings - * -[REDACTED_AUTHOR] - */ -class GetSupportFeedbackEmailUseCase( - private val feedbackRepository: FeedbackRepository, - private val resources: Resources, -) { - - // 00.00 00:00:00.000 - private val dateFormatter = DateTimeFormatterBuilder() - .appendDayOfMonth(2) - .appendLiteral('.') - .appendMonthOfYear(2) - .appendLiteral(' ') - .appendHourOfDay(2) - .appendLiteral(':') - .appendMinuteOfHour(2) - .appendLiteral(':') - .appendSecondOfMinute(2) - .appendLiteral('.') - .appendMillisOfSecond(3) - .toFormatter() - .withLocale(Locale.getDefault()) - - suspend operator fun invoke(): SupportFeedbackEmail { - val cardInfo = feedbackRepository.getCardInfo() - - return SupportFeedbackEmail( - address = getEmail(isStart2Coin = cardInfo.isStart2Coin), - subject = getSubject(isStart2Coin = cardInfo.isStart2Coin), - message = createMessage(cardInfo), - file = feedbackRepository.createLogFile(logs = getLogs()), - ) - } - - private fun getEmail(isStart2Coin: Boolean): String { - return if (isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL - } - - private fun getSubject(isStart2Coin: Boolean): String { - return resources.getString( - if (isStart2Coin) { - R.string.feedback_subject_support - } else { - R.string.feedback_subject_support_tangem - }, - ) - } - - private suspend fun createMessage(cardInfo: CardInfo): String { - return StringBuilder().apply { - append(resources.getString(R.string.feedback_preface_support)) - skipLine() - append(resources.getString(R.string.feedback_data_collection_message)) - skipLine() - append( - FeedbackDataBuilder().apply { - addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) - addDelimiter() - addCardInfo(cardInfo) - addDelimiter() - addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) - addDelimiter() - addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) - }.build(), - ) - }.toString() - } - - private suspend fun getLogs(): String { - val builder = StringBuilder() - - var sum = 0 - val appLogs = feedbackRepository.getAppLogs() - for (i in appLogs.lastIndex downTo 0) { - val log = appLogs[i] - val date = dateFormatter.print(DateTime(log.timestamp)) - - val formattedLog = "$date: ${log.message}\n" - - sum += formattedLog.length - if (sum < GMAIL_MAX_FILE_SIZE) { - builder.insert(0, formattedLog) - } else { - break - } - } - - return builder.toString() - } - - private companion object { - const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" - const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" - const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB - } -} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt new file mode 100644 index 0000000000..596699a073 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.repository.FeedbackRepository + +/** + * Save last blockchain error + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +class SaveBlockchainErrorUseCase( + private val feedbackRepository: FeedbackRepository, +) { + + fun invoke(error: BlockchainErrorInfo) { + feedbackRepository.saveBlockchainErrorInfo(error = error) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt new file mode 100644 index 0000000000..fc76ba451b --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.feedback.models + +/** + * Information about blockchain's operation error + * + * @property errorMessage message about error + * @property blockchainId blockchain id + * @property derivationPath derivation path + * @property destinationAddress destination address + * @property tokenSymbol token symbol or null, if it isn't operation with token + * @property amount amount + * @property fee fee or null, if unable to get + */ +data class BlockchainErrorInfo( + val errorMessage: String, + val blockchainId: String, + val derivationPath: String?, + val destinationAddress: String, + val tokenSymbol: String?, + val amount: String, + val fee: String?, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmail.kt similarity index 83% rename from domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt rename to domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmail.kt index 35faf18f6f..9187f590f0 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmail.kt @@ -2,7 +2,7 @@ package com.tangem.domain.feedback.models import java.io.File -data class SupportFeedbackEmail( +data class FeedbackEmail( val address: String, val subject: String, val message: String, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt new file mode 100644 index 0000000000..6bbd5a9670 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.feedback.models + +/** + * Email feedback type + * +[REDACTED_AUTHOR] + */ +sealed interface FeedbackEmailType { + + /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ + data object DirectUserRequest : FeedbackEmailType + + /** User rate the app as "can be better" */ + data object RateCanBeBetter : FeedbackEmailType + + /** User has problem with scanning */ + data object ScanningProblem : FeedbackEmailType + + /** User has problem with sending transaction */ + data object TransactionSendingProblem : FeedbackEmailType +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 58281067c1..c9d6806eea 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -11,8 +11,14 @@ interface FeedbackRepository { suspend fun getBlockchainInfoList(): List + suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? + fun getPhoneInfo(): PhoneInfo + fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) + + suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? + suspend fun getAppLogs(): List suspend fun createLogFile(logs: String): File? diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt new file mode 100644 index 0000000000..e5596b2a78 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.feedback.utils + +import com.tangem.domain.feedback.models.AppLogModel +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormatter +import org.joda.time.format.DateTimeFormatterBuilder +import java.util.Locale + +/** + * App logs formatter + * +[REDACTED_AUTHOR] + */ +internal class AppLogsFormatter { + + private val dateFormatter = createDateFormatter() + + /** Format [appLogs] to [String] */ + fun format(appLogs: List): String { + val builder = StringBuilder() + + var sum = 0 + for (i in appLogs.lastIndex downTo 0) { + val log = appLogs[i] + val date = dateFormatter.print(DateTime(log.timestamp)) + + val formattedLog = "$date: ${log.message}\n" + + sum += formattedLog.length + if (sum < GMAIL_MAX_FILE_SIZE) { + builder.insert(0, formattedLog) + } else { + break + } + } + + return builder.toString() + } + + // Example, 00.00 00:00:00.000 + private fun createDateFormatter(): DateTimeFormatter { + return DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral('.') + .appendMonthOfYear(2) + .appendLiteral(' ') + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral('.') + .appendMillisOfSecond(MIN_MILLIS_DIGITS) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + private companion object { + const val MIN_MILLIS_DIGITS = 3 + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt new file mode 100644 index 0000000000..8574fb1010 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -0,0 +1,70 @@ +package com.tangem.domain.feedback.utils + +import com.tangem.domain.feedback.FeedbackDataBuilder +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.repository.FeedbackRepository + +/** + * Email message body resolver + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +internal class EmailMessageBodyResolver( + private val feedbackRepository: FeedbackRepository, +) { + + /** Resolve email message body by [type] using [cardInfo] */ + suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) { + when (type) { + FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo) + FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo) + FeedbackEmailType.ScanningProblem -> addScanningProblemBody() + FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo) + } + + return build() + } + + private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addDelimiter() + addCardInfo(cardInfo) + addDelimiter() + addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private fun FeedbackDataBuilder.addScanningProblemBody() { + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) { + addCardInfo(cardInfo) + addDelimiter() + + val blockchainError = feedbackRepository.getBlockchainErrorInfo() + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { + addCardInfo(cardInfo) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt new file mode 100644 index 0000000000..38c983c617 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.feedback.utils + +import android.content.res.Resources +import com.tangem.domain.feedback.R +import com.tangem.domain.feedback.models.FeedbackEmailType + +/** + * Email message title resolver + * + * @property resources resources + * +[REDACTED_AUTHOR] + */ +internal class EmailMessageTitleResolver(private val resources: Resources) { + + /** Resolve email message title by [type] */ + fun resolve(type: FeedbackEmailType): String { + return when (type) { + FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support + FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative + FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed + FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + } + .let(resources::getString) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt new file mode 100644 index 0000000000..392fa7d2f4 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.feedback.utils + +import android.content.res.Resources +import com.tangem.domain.feedback.R +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmailType + +/** + * Email subject resolver + * + * @property resources resources + * +[REDACTED_AUTHOR] + */ +internal class EmailSubjectResolver(private val resources: Resources) { + + /** Resolve email message body by [type] using [cardInfo] */ + fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String { + return when (type) { + FeedbackEmailType.DirectUserRequest -> { + if (cardInfo.isStart2Coin) { + R.string.feedback_subject_support + } else { + R.string.feedback_subject_support_tangem + } + } + FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative + FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed + FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + } + .let(resources::getString) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt new file mode 100644 index 0000000000..aeaa917e5f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.feedback.utils + +internal const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB + +internal const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" +internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index bc0e716a7a..f01a2e6a96 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -9,16 +9,17 @@ android { } dependencies { - implementation(project(":core:datasource")) - implementation(project(":core:utils")) - implementation(project(":common")) - implementation(project(":libs:auth")) + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.common) + implementation(projects.libs.auth) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) - /** Tangem libraries */ implementation(deps.tangem.blockchain) { exclude(module = "joda-time") diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 56a874bd6a..55cc073069 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -22,6 +22,10 @@ interface CardTypesResolver { fun isKaspaWallet(): Boolean + fun isKaspa2Wallet(): Boolean + + fun isKaspaResellerWallet(): Boolean + fun isBadWallet(): Boolean fun isJrWallet(): Boolean @@ -36,6 +40,24 @@ interface CardTypesResolver { fun isNewWorldEliteWallet(): Boolean + fun isRedPandaWallet(): Boolean + + fun isCryptoSethWallet(): Boolean + + fun isKishuInuWallet(): Boolean + + fun isBabyDogeWallet(): Boolean + + fun isCOQWallet(): Boolean + + fun isCoinMetricaWallet(): Boolean + + fun isVoltInuWallet(): Boolean + + fun isVividWallet(): Boolean + + fun isPastelWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 1f0b7b7b51..9373ae2a5b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -20,6 +20,5 @@ object NetworkLogConfig { object AnalyticsHandlersLogConfig { const val firebase: Boolean = false - const val appsFlyer: Boolean = false val amplitude: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 8c3a6ec485..82208040e8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -40,6 +40,10 @@ internal class TangemCardTypesResolver( override fun isKaspaWallet(): Boolean = card.batchId == KASPA_WALLET_BATCH_ID + override fun isKaspa2Wallet(): Boolean = card.batchId == KASPA2_WALLET_BATCH_ID + + override fun isKaspaResellerWallet(): Boolean = card.batchId == KASPA_RESELLER_WALLET_BATCH_ID + override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID @@ -54,6 +58,28 @@ internal class TangemCardTypesResolver( override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID + override fun isRedPandaWallet(): Boolean = card.batchId == RED_PANDA_WALLET_BATCH_ID + + override fun isCryptoSethWallet(): Boolean = card.batchId == CRYPTO_SETH_WALLET_BATCH_ID + + override fun isKishuInuWallet(): Boolean = card.batchId == KISHU_INU_WALLET_BATCH_ID + + override fun isBabyDogeWallet(): Boolean = card.batchId == BABY_DOGE_WALLET_BATCH_ID + + override fun isCOQWallet(): Boolean = card.batchId == COQ_WALLET_BATCH_ID + + override fun isCoinMetricaWallet(): Boolean = card.batchId == COIN_METRICA_WALLET_BATCH_ID + + override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID + + override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID || + card.batchId == VIVID_AQUA_WALLET_BATCH_ID || + card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID + + override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID || + card.batchId == PASTEL_GRASS_WALLET_BATCH_ID || + card.batchId == PASTEL_AIR_WALLET_BATCH_ID + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -153,6 +179,8 @@ internal class TangemCardTypesResolver( const val DEV_KIT_CARD_BATCH_ID = "CB83" const val TRON_WALLET_BATCH_ID = "AF07" const val KASPA_WALLET_BATCH_ID = "AF08" + const val KASPA2_WALLET_BATCH_ID = "AF25" + const val KASPA_RESELLER_WALLET_BATCH_ID = "AF31" const val BAD_WALLET_BATCH_ID = "AF09" const val JR_WALLET_BATCH_ID = "AF14" const val GRIM_WALLET_BATCH_ID = "AF13" @@ -163,5 +191,20 @@ internal class TangemCardTypesResolver( const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33" const val VECHAIN_WALLET_BATCH_ID = "AF29" const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26" + const val RED_PANDA_WALLET_BATCH_ID = "AF34" + const val CRYPTO_SETH_WALLET_BATCH_ID = "AF32" + const val KISHU_INU_WALLET_BATCH_ID = "AF52" + const val BABY_DOGE_WALLET_BATCH_ID = "AF51" + const val COQ_WALLET_BATCH_ID = "AF28" + const val COIN_METRICA_WALLET_BATCH_ID = "AF27" + const val VOLT_INU_WALLET_BATCH_ID = "AF35" + // VIVID WALLETS + const val VIVID_LEMON_WALLET_BATCH_ID = "AF40" + const val VIVID_AQUA_WALLET_BATCH_ID = "AF41" + const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42" + // PASTEL WALLETS + const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43" + const val PASTEL_AIR_WALLET_BATCH_ID = "AF44" + const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45" } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index aea826fa7c..cf827af989 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -1,6 +1,7 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.CardTypesResolver @@ -22,7 +23,7 @@ fun CardDTO.supportedBlockchains(cardTypesResolver: CardTypesResolver): List TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } }, ) .let(txHistoryStateConverter::convert) @@ -221,7 +223,16 @@ class DefaultWalletManagersFacade( pageSize = pageSize, filterType = when (currency) { is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } }, ), ) @@ -230,7 +241,8 @@ class DefaultWalletManagersFacade( is Result.Success -> PaginationWrapper( currentPage = sdkPageConverter.convert(page), nextPage = sdkPageConverter.convert(itemsResult.data.nextPage), - items = txHistoryItemConverter.convertList(itemsResult.data.items), + items = SdkTransactionHistoryItemConverter(smartContractMethods = readSmartContractMethods()) + .convertList(itemsResult.data.items), ) is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage) } @@ -324,24 +336,25 @@ class DefaultWalletManagersFacade( blockchain: Blockchain, derivationPath: String?, ): WalletManager? { - val userWallet = getUserWallet(userWalletId) - var walletManager = walletManagersStore.getSyncOrNull( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = derivationPath, - ) - - if (walletManager == null) { - walletManager = walletManagerFactory.createWalletManager( - scanResponse = userWallet.scanResponse, + initMutex.withLock { + val userWallet = getUserWallet(userWalletId) + var walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, blockchain = blockchain, - derivationPath = derivationPath?.let { DerivationPath(rawPath = it) }, - ) ?: return null + derivationPath = derivationPath, + ) + if (walletManager == null) { + walletManager = walletManagerFactory.createWalletManager( + scanResponse = userWallet.scanResponse, + blockchain = blockchain, + derivationPath = derivationPath?.let { DerivationPath(rawPath = it) }, + ) + walletManager ?: return null - walletManagersStore.store(userWalletId, walletManager) + walletManagersStore.store(userWalletId, walletManager) + } + return walletManager } - - return walletManager } @Deprecated("Will be removed in future") @@ -442,12 +455,12 @@ class DefaultWalletManagersFacade( destination: String, userWalletId: UserWalletId, network: Network, - ): Result? { + ): Result? = withContext(dispatchers.io) { val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, network = network, ) - return (walletManager as? TransactionSender)?.getFee( + (walletManager as? TransactionSender)?.getFee( amount = amount, destination = destination, ) @@ -504,20 +517,6 @@ class DefaultWalletManagersFacade( return walletManager?.createTransaction(amount, fee, destination) } - @Deprecated("Will be removed in future") - override suspend fun sendTransaction( - txData: TransactionData, - signer: CommonSigner, - userWalletId: UserWalletId, - network: Network, - ): SimpleResult { - val walletManager = getOrCreateWalletManager( - userWalletId = userWalletId, - network = network, - ) - return (walletManager as TransactionSender).send(txData, signer) - } - override suspend fun getRecentTransactions( userWalletId: UserWalletId, currency: CryptoCurrency, @@ -573,6 +572,45 @@ class DefaultWalletManagersFacade( ) } + override suspend fun getAssetRequirements( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): AssetRequirementsCondition? { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null + + val condition = walletManager.requirementsCondition(currencyType) ?: return null + return requirementsConditionConverter.convert(condition) + } + + override suspend fun associateAsset( + userWalletId: UserWalletId, + currency: CryptoCurrency, + signer: CommonSigner, + ): SimpleResult { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + + if (walletManager !is AssetRequirementsManager) { + return SimpleResult.Failure( + BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"), + ) + } + return walletManager.fulfillRequirements(currencyType, signer) + } + + override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) ?: return false + + return (walletManager as? UtxoBlockchainManager)?.allowConsolidation == true + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return @@ -582,4 +620,8 @@ class DefaultWalletManagersFacade( walletManager.addTokens(tokensToAdd) } + + private suspend fun readSmartContractMethods(): Map { + return assetLoader.loadMap(fileName = "contract_methods") + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4636317765..4ecfbe3805 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -13,6 +13,7 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState @@ -211,22 +212,6 @@ interface WalletManagersFacade { network: Network, ): TransactionData? - /** - * Sends transaction - * - * @param txData transaction data - * @param signer card signer - * @param userWalletId selected wallet id - * @param network network of currency - */ - @Deprecated("Will be removed in future") - suspend fun sendTransaction( - txData: TransactionData, - signer: CommonSigner, - userWalletId: UserWalletId, - network: Network, - ): SimpleResult - /** Get recent transactions of [userWalletId] for [currency] */ suspend fun getRecentTransactions(userWalletId: UserWalletId, currency: CryptoCurrency): List @@ -240,4 +225,24 @@ interface WalletManagersFacade { decimals: Int, id: String? = null, ): BigDecimal + + /** + * Get requirements for asset(currency) + * @return null if there's no requirement, otherwise [AssetRequirementsCondition]. + */ + suspend fun getAssetRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): AssetRequirementsCondition? + + suspend fun associateAsset( + userWalletId: UserWalletId, + currency: CryptoCurrency, + signer: CommonSigner, + ): SimpleResult + + /** + * Indicates UTXO consolidation availability + * + * @param userWalletId selected user wallet + * @param network availability for network + */ + suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt new file mode 100644 index 0000000000..53d7f303c5 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.CryptoCurrencyType +import com.tangem.blockchain.common.Token +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyTypeConverter : Converter { + override fun convert(value: CryptoCurrency): CryptoCurrencyType { + return when (value) { + is CryptoCurrency.Coin -> CryptoCurrencyType.Coin + is CryptoCurrency.Token -> CryptoCurrencyType.Token( + info = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.rawCurrencyId, + ), + ) + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt new file mode 100644 index 0000000000..7c84120470 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.domain.transaction.models.AssetRequirementsCondition +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition as SdkRequirementsCondition + +internal class SdkRequirementsConditionConverter : Converter { + override fun convert(value: SdkRequirementsCondition): AssetRequirementsCondition { + return when (value) { + SdkRequirementsCondition.PaidTransaction -> AssetRequirementsCondition.PaidTransaction + is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee( + feeAmount = requireNotNull(value.feeAmount.value), + feeCurrencySymbol = value.feeAmount.currencySymbol, + decimals = value.feeAmount.decimals, + ) + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 2a8c4248b7..a429859de4 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -1,20 +1,16 @@ package com.tangem.domain.walletmanager.utils -import com.squareup.moshi.Moshi -import com.tangem.blockchain.common.txhistory.TransactionHistoryItem -import com.tangem.datasource.asset.AssetReader +import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.utils.converter.Converter -import com.tangem.blockchain.common.txhistory.TransactionHistoryItem as SdkTransactionHistoryItem +import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem as SdkTransactionHistoryItem internal class SdkTransactionHistoryItemConverter( - assetReader: AssetReader, - moshi: Moshi, + smartContractMethods: Map, ) : Converter { - private val typeConverter by lazy { - SdkTransactionTypeConverter(assetReader, moshi) - } + private val typeConverter by lazy { SdkTransactionTypeConverter(smartContractMethods) } override fun convert(value: SdkTransactionHistoryItem): TxHistoryItem = TxHistoryItem( txHash = value.txHash, @@ -58,7 +54,9 @@ internal class SdkTransactionHistoryItemConverter( } else { mapToInteractionAddressType(sourceType = sourceType) } - is SdkTransactionHistoryItem.TransactionType.ContractMethod -> mapToInteractionAddressType(destinationType) + is SdkTransactionHistoryItem.TransactionType.ContractMethod, + is SdkTransactionHistoryItem.TransactionType.ContractMethodName, + -> mapToInteractionAddressType(destinationType) } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt index 6750e05410..6532b6338f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryStateConverter.kt @@ -1,9 +1,9 @@ package com.tangem.domain.walletmanager.utils -import com.tangem.blockchain.common.txhistory.TransactionHistoryState +import com.tangem.blockchain.transactionhistory.TransactionHistoryState import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.utils.converter.Converter -import com.tangem.blockchain.common.txhistory.TransactionHistoryState as SdkTransactionHistoryState +import com.tangem.blockchain.transactionhistory.TransactionHistoryState as SdkTransactionHistoryState internal class SdkTransactionHistoryStateConverter : Converter { diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt index 67019252a1..c0ebfa9a66 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -1,47 +1,30 @@ package com.tangem.domain.walletmanager.utils -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.blockchain.common.txhistory.TransactionHistoryItem -import com.tangem.datasource.asset.AssetReader +import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.utils.converter.Converter -class SdkTransactionTypeConverter( - private val assetReader: AssetReader, - private val moshi: Moshi, +internal class SdkTransactionTypeConverter( + private val smartContractMethods: Map, ) : Converter { - private val adapter: JsonAdapter> by lazy { - moshi.adapter( - Types.newParameterizedType( - Map::class.java, - String::class.java, - SmartContractMethod::class.java, - ), - ) - } - private val smartContractMethods by lazy { readSmartContractMethods() } - override fun convert(value: TransactionHistoryItem.TransactionType): TxHistoryItem.TransactionType { return when (value) { - TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer - is TransactionHistoryItem.TransactionType.ContractMethod -> { - return when (val name = smartContractMethods[value.id]?.name) { - "transfer" -> TxHistoryItem.TransactionType.Transfer - "approve" -> TxHistoryItem.TransactionType.Approve - "swap" -> TxHistoryItem.TransactionType.Swap - null -> TxHistoryItem.TransactionType.UnknownOperation - else -> TxHistoryItem.TransactionType.Operation(name = name.replaceFirstChar { it.titlecase() }) - } - } + is TransactionHistoryItem.TransactionType.ContractMethod -> + getTransactionType(methodName = smartContractMethods[value.id]?.name) + is TransactionHistoryItem.TransactionType.ContractMethodName -> getTransactionType(methodName = value.name) + is TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer } } - private fun readSmartContractMethods(): Map { - val json = assetReader.readJson("contract_methods") - return adapter.fromJson(json) ?: emptyMap() + private fun getTransactionType(methodName: String?): TxHistoryItem.TransactionType { + return when (methodName) { + "transfer" -> TxHistoryItem.TransactionType.Transfer + "approve" -> TxHistoryItem.TransactionType.Approve + "swap" -> TxHistoryItem.TransactionType.Swap + null -> TxHistoryItem.TransactionType.UnknownOperation + else -> TxHistoryItem.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() }) + } } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index 381b9cadd3..cd756efd3c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -66,6 +66,7 @@ internal class TransactionDataToTxHistoryItemConverter( value } } + is FeePaidCurrency.FeeResource -> value } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 6b88794b4b..84b194166b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -1,7 +1,7 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* -import com.tangem.domain.common.extensions.amountToCreateAccount +import com.tangem.blockchainsdk.utils.amountToCreateAccount import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction @@ -114,7 +114,9 @@ internal class UpdateWalletManagerResultFactory { is AmountType.Coin -> CryptoCurrencyAmount.Coin( value = getCurrencyAmountValue(amount) ?: return null, ) - is AmountType.Reserve -> null + is AmountType.FeeResource, + AmountType.Reserve, + -> null } } @@ -135,7 +137,9 @@ internal class UpdateWalletManagerResultFactory { txHistoryItem = txHistoryItem, ) } - is AmountType.Reserve -> null + is AmountType.FeeResource, + AmountType.Reserve, + -> null } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt index 89a63b05f6..c5b825b82c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -1,37 +1,21 @@ package com.tangem.domain.walletmanager.utils -import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.makeWalletManagerForApp import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import timber.log.Timber -import com.tangem.blockchain.common.WalletManagerFactory as BlockchainWalletManagerFactory internal class WalletManagerFactory( - configManager: ConfigManager, - accountCreator: AccountCreator, - blockchainDataStorage: BlockchainDataStorage, - blockchainSDKLogger: BlockchainSDKLogger? = null, + private val blockchainSDKFactory: BlockchainSDKFactory, ) { - private val sdkWalletManagerFactory by lazy { - BlockchainWalletManagerFactory( - config = configManager.config.blockchainSdkConfig, - accountCreator = accountCreator, - blockchainDataStorage = blockchainDataStorage, - loggers = listOfNotNull(blockchainSDKLogger), - ) - } - - fun createWalletManager( + suspend fun createWalletManager( scanResponse: ScanResponse, blockchain: Blockchain, derivationPath: DerivationPath?, @@ -39,7 +23,7 @@ internal class WalletManagerFactory( val derivationParams = getDerivationParams(derivationPath, scanResponse.derivationStyleProvider) return try { - sdkWalletManagerFactory.makeWalletManagerForApp( + blockchainSDKFactory.getWalletManagerFactorySync()?.makeWalletManagerForApp( scanResponse = scanResponse, blockchain = blockchain, derivationParams = derivationParams, diff --git a/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt b/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt index ff7740c1ba..89ba2bf8ad 100644 --- a/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt +++ b/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt @@ -2,14 +2,14 @@ package com.tangem.domain.features import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toNetworkId import org.junit.Test class BlockchainTests { @Test fun allNetworkIdsAreImplemented() { - val unimplementedIds = Blockchain.values() + val unimplementedIds = Blockchain.entries .toMutableList() .apply { remove(Blockchain.Unknown) diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IncrementAppLaunchCounterUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IncrementAppLaunchCounterUseCase.kt new file mode 100644 index 0000000000..c429dc2c66 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IncrementAppLaunchCounterUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.SettingsRepository + +class IncrementAppLaunchCounterUseCase( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke(): Either { + return Either.catch { settingsRepository.incrementAppLaunchCounter() } + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetSaveWalletScreenShownUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetSaveWalletScreenShownUseCase.kt new file mode 100644 index 0000000000..593b82a683 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetSaveWalletScreenShownUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.SettingsRepository + +class SetSaveWalletScreenShownUseCase( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke(): Either { + return Either.catch { + settingsRepository.setShouldShowSaveUserWalletScreen(value = false) + } + } +} \ No newline at end of file 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 63a3ab3247..3b5f6d3c40 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 @@ -4,6 +4,8 @@ interface SettingsRepository { suspend fun shouldShowSaveUserWalletScreen(): Boolean + suspend fun setShouldShowSaveUserWalletScreen(value: Boolean) + suspend fun isWalletScrollPreviewEnabled(): Boolean suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) @@ -17,4 +19,18 @@ interface SettingsRepository { suspend fun isSendTapHelpPreviewEnabled(): Boolean suspend fun setSendTapHelpPreviewAvailability(isEnabled: Boolean) + + suspend fun wasApplicationStopped(): Boolean + + suspend fun setWasApplicationStopped(value: Boolean) + + suspend fun shouldOpenWelcomeScreenOnResume(): Boolean + + suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) + + suspend fun shouldSaveAccessCodes(): Boolean + + suspend fun setShouldSaveAccessCodes(value: Boolean) + + suspend fun incrementAppLaunchCounter() } \ No newline at end of file diff --git a/domain/staking/.gitignore b/domain/staking/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/staking/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts new file mode 100644 index 0000000000..f6a93898bb --- /dev/null +++ b/domain/staking/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt new file mode 100644 index 0000000000..18717a31f7 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.staking + +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for getting info about staking availability for certain blockchain. + */ +class GetStakingAvailabilityUseCase( + private val stakingRepository: StakingRepository, +) { + + operator fun invoke(blockchainNetworkId: String): StakingAvailability { + return stakingRepository.getStakingAvailability(blockchainNetworkId) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt new file mode 100644 index 0000000000..02a505aa03 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for getting entry info about staking on token screen. + */ +class GetStakingEntryInfoUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke(integrationId: String): Either { + return Either.catch { stakingRepository.getEntryInfo(integrationId) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt new file mode 100644 index 0000000000..bf40ee3b58 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.staking.model + +sealed class StakingAvailability { + + data class Available(val integrationId: String) : StakingAvailability() + + data object Unavailable : StakingAvailability() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt new file mode 100644 index 0000000000..691aece740 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.staking.model + +import java.math.BigDecimal + +data class StakingEntryInfo( + val interestRate: BigDecimal, + val periodInDays: Int, + val tokenSymbol: String, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..763377ac5e --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.staking.repositories + +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo + +interface StakingRepository { + + fun getStakingAvailability(blockchainId: String): StakingAvailability + + suspend fun getEntryInfo(integrationId: String): StakingEntryInfo +} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 3e0bb9743c..de3ef835c5 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -11,11 +11,13 @@ android { dependencies { /** Project - Domain */ - implementation(projects.domain.core) + api(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) implementation(projects.domain.settings) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 58eaf3cff6..6b098d5be3 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -15,7 +15,7 @@ import java.math.BigDecimal */ data class CryptoCurrencyStatus( val currency: CryptoCurrency, - val value: Status, + val value: Value, ) { /** @@ -23,7 +23,7 @@ data class CryptoCurrencyStatus( * * @property isError Indicates whether this status represents an error status. */ - sealed class Status(val isError: Boolean) { + sealed class Value(val isError: Boolean) { /** The amount of the cryptocurrency. */ open val amount: BigDecimal? = null @@ -48,7 +48,7 @@ data class CryptoCurrencyStatus( } /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ - object Loading : Status(isError = false) + data object Loading : Value(isError = false) /** * Represents a state where the cryptocurrency is not reachable. @@ -61,19 +61,19 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, override val networkAddress: NetworkAddress?, - ) : Status(isError = true) + ) : Value(isError = true) /** Represents a state where the cryptocurrency's network amount not found. */ data class NoAmount( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, - ) : Status(isError = true) + ) : Value(isError = true) /** Represents a state where the cryptocurrency's derivation is missed. */ data class MissedDerivation( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, - ) : Status(isError = true) + ) : Value(isError = true) /** * Represents a state where there is no account associated with the cryptocurrency @@ -86,7 +86,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, override val networkAddress: NetworkAddress, - ) : Status(isError = false) { + ) : Value(isError = false) { override val amount: BigDecimal = BigDecimal.ZERO } @@ -110,7 +110,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, - ) : Status(isError = false) + ) : Value(isError = false) /** * Represents a Custom state of a cryptocurrency, typically used for user-defined tokens. @@ -131,7 +131,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, - ) : Status(isError = false) + ) : Value(isError = false) /** * Represents a state where the cryptocurrency is available, but there is no current quote available for it. @@ -146,5 +146,5 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, - ) : Status(isError = false) + ) : Value(isError = false) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FeePaidCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FeePaidCurrency.kt index dba9b23181..e61ffb4e2f 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FeePaidCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/FeePaidCurrency.kt @@ -5,6 +5,7 @@ import java.math.BigDecimal sealed class FeePaidCurrency { data object Coin : FeePaidCurrency() data object SameCurrency : FeePaidCurrency() + data class FeeResource(val currency: String) : FeePaidCurrency() data class Token( val tokenId: CryptoCurrency.ID, val name: String, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 17274cfbe9..5dcdb275ef 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -16,6 +16,9 @@ import kotlinx.parcelize.Parcelize * @property derivationPath The path used to derive keys for this network. * @property isTestnet Indicates whether the network is a test network or a main network. * @property standardType The type of blockchain standard the network adheres to. + * @property hasFiatFeeRate Indicates whether there is a fee in the network + * that cannot be represented in a fiat currency. + * (For those blockchains that have FeeResource instead of a standard type of fee) */ @Parcelize data class Network( @@ -26,6 +29,7 @@ data class Network( val derivationPath: DerivationPath, val isTestnet: Boolean, val standardType: StandardType, + val hasFiatFeeRate: Boolean, ) : Parcelable { init { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index 60fdf9e7b8..08a746b12e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -11,7 +11,7 @@ import java.math.BigDecimal */ data class NetworkStatus( val network: Network, - val value: Status, + val value: Value, ) { /** @@ -19,19 +19,19 @@ data class NetworkStatus( * * This sealed class includes different states like unreachable, missed derivation, verified, and no account. */ - sealed class Status + sealed class Value /** * Represents the state where the network is unreachable. * * @property address Network addresses. */ - data class Unreachable(val address: NetworkAddress?) : Status() + data class Unreachable(val address: NetworkAddress?) : Value() /** * Represents the state where a derivation has been missed. */ - object MissedDerivation : Status() + data object MissedDerivation : Value() /** * Represents the verified state of the network, including the amounts associated with different cryptocurrencies @@ -46,7 +46,7 @@ data class NetworkStatus( val address: NetworkAddress, val amounts: Map, val pendingTransactions: Map>, - ) : Status() + ) : Value() /** * Represents the state where there is no account, and an amount is required to create one. @@ -59,5 +59,5 @@ data class NetworkStatus( val address: NetworkAddress, val amountToCreateAccount: BigDecimal, val errorMessage: String, - ) : Status() + ) : Value() } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 36d186b41d..2fa13a5018 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -31,7 +31,7 @@ sealed class CryptoCurrencyWarning { val amountCurrency: CryptoCurrency, ) : CryptoCurrencyWarning() - object TopUpWithoutReserve : CryptoCurrencyWarning() + data object TopUpWithoutReserve : CryptoCurrencyWarning() /** * Represents wallet blockchain rent @@ -41,12 +41,18 @@ sealed class CryptoCurrencyWarning { */ data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning() - data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning() - data class SwapPromo( val startDateTime: DateTime, val endDateTime: DateTime, ) : CryptoCurrencyWarning() data object BeaconChainShutdown : CryptoCurrencyWarning() + + /** + * Shows a warning about an available fee resource for a transaction in several blockchains (ex. Koinos) + */ + data class FeeResourceInfo( + val amount: BigDecimal, + val maxAmount: BigDecimal?, + ) : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt new file mode 100644 index 0000000000..e251aae9a0 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +sealed class HederaWarnings : CryptoCurrencyWarning() { + + abstract val currency: CryptoCurrency + + data class AssociateWarning(override val currency: CryptoCurrency) : HederaWarnings() + + data class AssociateWarningWithFee( + override val currency: CryptoCurrency, + val fee: BigDecimal, + val feeCurrencySymbol: String, + val feeCurrencyDecimals: Int, + ) : HederaWarnings() +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt index ed01f84f51..171f60eb9e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt @@ -72,4 +72,9 @@ sealed class TokenScreenAnalyticsEvent( event = "Token Bought", params = mapOf("Token" to token), ) + + class Associate(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent( + event = "Button - Token Trustline", + params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), + ) } \ 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 21613763a1..9febf735fe 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 @@ -7,6 +7,7 @@ import arrow.core.raise.either import arrow.core.toNonEmptyListOrNull import com.tangem.domain.tokens.error.AddCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.models.UserWalletId @@ -37,6 +38,26 @@ class AddCryptoCurrenciesUseCase( return invoke(userWalletId, listOf(currency)) } + /** + * Adds a [cryptoCurrency] token with specific [network] and derivation to the wallet identified by [userWalletId]. + * + * After successfully adding a currency, it also refreshes the networks for tokens + * that are being added and have corresponding coins in the existing currencies list. + * + * @param userWalletId The ID of the user's wallet. + * @param cryptoCurrency Token to add. + * @param network Network where we add + * @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation. + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency.Token, + network: Network, + ): Either = either { + val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) + invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) + } + /** * Adds a list of [currencies] to the wallet identified by [userWalletId]. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt index efe0126403..31b6ccc631 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens -import arrow.core.Either import arrow.core.left +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -12,19 +12,19 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.transformLatest class GetCardTokensListUseCase( - internal val currenciesRepository: CurrenciesRepository, - internal val quotesRepository: QuotesRepository, - internal val networksRepository: NetworksRepository, - internal val dispatchers: CoroutineDispatcherProvider, + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): Flow> { + operator fun invoke(userWalletId: UserWalletId): EitherFlow { return getTokensStatuses(userWalletId).transformLatest { maybeTokens -> maybeTokens.fold( ifLeft = { error -> @@ -37,9 +37,7 @@ class GetCardTokensListUseCase( } } - private fun getTokensStatuses( - userWalletId: UserWalletId, - ): Flow>> { + private fun getTokensStatuses(userWalletId: UserWalletId): EitherFlow> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -56,7 +54,7 @@ class GetCardTokensListUseCase( private fun createTokenList( userWalletId: UserWalletId, tokens: List, - ): Flow> { + ): EitherFlow { val operations = TokenListOperations( userWalletId = userWalletId, tokens = tokens, 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 07cdfdabdc..e521d035af 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 @@ -8,14 +8,12 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import java.math.BigDecimal /** * Use case to determine which TokenActions are available for a [CryptoCurrency] @@ -25,16 +23,19 @@ import java.math.BigDecimal @Suppress("LongParameterList") class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, + private val walletManagersFacade: WalletManagersFacade, private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, - private val sendFeatureToggles: SendFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -42,7 +43,7 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id - + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency) return flow { val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) @@ -57,6 +58,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, + needAssociateAsset = requirements != null, ) } @@ -68,33 +70,37 @@ class GetCryptoCurrencyActionsUseCase( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): TokenActionsState { return TokenActionsState( walletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, states = createListOfActions( - userWallet, - coinStatus, - cryptoCurrencyStatus, + userWallet = userWallet, + coinStatus = coinStatus, + cryptoCurrencyStatus = cryptoCurrencyStatus, + needAssociateAsset = needAssociateAsset, ), ) } /** * Creates list of action for expected order - * Actions priority: [Buy Send Receive Sell Swap] + * Actions priority: [Receive Send Swap Buy Sell] */ + @Suppress("CyclomaticComplexMethod", "LongMethod") private suspend fun createListOfActions( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { - return listOf(TokenActionsState.ActionState.HideToken(true)) + return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) } if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(cryptoCurrencyStatus) + return getActionsForUnreachableCurrency(cryptoCurrencyStatus, needAssociateAsset) } val activeList = mutableListOf() @@ -102,115 +108,155 @@ class GetCryptoCurrencyActionsUseCase( // copy address if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(true)) + activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } // receive if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.Receive(true)) + val scenario = if (needAssociateAsset) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + activeList.add(TokenActionsState.ActionState.Receive(scenario)) } // send - if ( - isSendDisabled( - userWalletId = userWallet.walletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) - ) { - disabledList.add(TokenActionsState.ActionState.Send(false)) + val sendUnavailabilityReason = getSendUnavailabilityReason( + cryptoCurrencyStatus = cryptoCurrencyStatus, + coinStatus = coinStatus, + ) + if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason)) } else { - activeList.add(TokenActionsState.ActionState.Send(true)) + disabledList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason)) } // swap if (userWallet.isMultiCurrency) { - if (marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Swap(true)) + if ( + marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency) && + cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote + ) { + activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Swap(false)) + disabledList.add( + TokenActionsState.ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.NotExchangeable(cryptoCurrency.name), + ), + ) } } // buy if (rampManager.availableForBuy(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Buy(true)) + activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Buy(false)) + disabledList.add( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrency.symbol)), + ) } // sell - if (rampManager.availableForSell(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Sell(true)) - } else { - disabledList.add(TokenActionsState.ActionState.Sell(false)) + val sellSupportedByService = rampManager.availableForSell(cryptoCurrency) + val sendAvailable = sendUnavailabilityReason is ScenarioUnavailabilityReason.None + + when { + sellSupportedByService && sendAvailable -> { + activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) + } + sellSupportedByService && !sendAvailable -> { + (sendUnavailabilityReason as? ScenarioUnavailabilityReason.EmptyBalance)?.let { + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = it.copy( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL, + ), + ), + ) + } + (sendUnavailabilityReason as? ScenarioUnavailabilityReason.PendingTransaction)?.let { + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = it.copy( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL, + ), + ), + ) + } + } + else -> { + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = ScenarioUnavailabilityReason.NotSupportedBySellService( + cryptoCurrency.name, + ), + ), + ) + } } // hide - activeList.add(TokenActionsState.ActionState.HideToken(true)) + activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return activeList + disabledList } private fun getActionsForUnreachableCurrency( cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): List { - val activeList = mutableListOf() - val disabledList = mutableListOf() + val actionsList = mutableListOf() if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(true)) + actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } if (rampManager.availableForBuy(cryptoCurrencyStatus.currency)) { - activeList.add(TokenActionsState.ActionState.Buy(true)) + actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Buy(false)) + actionsList.add( + TokenActionsState.ActionState.Buy( + ScenarioUnavailabilityReason.BuyUnavailable( + cryptoCurrencyName = cryptoCurrencyStatus.currency.name, + ), + ), + ) } - disabledList.add(TokenActionsState.ActionState.Send(false)) - disabledList.add(TokenActionsState.ActionState.Swap(false)) - disabledList.add(TokenActionsState.ActionState.Sell(false)) + actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable)) + actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.Unreachable)) + actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.Receive(true)) + val scenario = if (needAssociateAsset) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } - activeList.add(TokenActionsState.ActionState.HideToken(true)) - return activeList + disabledList + actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) + return actionsList } - private suspend fun isSendDisabled( - userWalletId: UserWalletId, + private fun getSendUnavailabilityReason( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, - ): Boolean { - val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrencyStatus.currency) - val notEnoughBalanceForFee = isNotEnoughBalanceForFee( - feePaidCurrency = feePaidCurrency, - tokenStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) - return cryptoCurrencyStatus.value.amount.isNullOrZero() || - notEnoughBalanceForFee || + ): ScenarioUnavailabilityReason { + return when { + cryptoCurrencyStatus.value.amount.isNullOrZero() -> { + ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) + } currenciesRepository.hasPendingTransactions( cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus, - ) - } - - private fun isNotEnoughBalanceForFee( - feePaidCurrency: FeePaidCurrency, - tokenStatus: CryptoCurrencyStatus, - coinStatus: CryptoCurrencyStatus?, - ): Boolean { - return if (sendFeatureToggles.isRedesignedSendEnabled) { - tokenStatus.value.amount.isZero() - } else { - when (feePaidCurrency) { - FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero() - FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero() - is FeePaidCurrency.Token -> { - val feePaidTokenBalance = feePaidCurrency.balance - !tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero() - } + ) -> { + ScenarioUnavailabilityReason.PendingTransaction( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND, + networkName = coinStatus?.currency?.network?.name.orEmpty(), + ) + } + else -> { + ScenarioUnavailabilityReason.None } } } @@ -218,8 +264,4 @@ class GetCryptoCurrencyActionsUseCase( private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } - - private fun BigDecimal?.isZero(): Boolean { - return this?.signum() == 0 - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt index ed3cbfad6c..8150cfc734 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -22,6 +22,7 @@ class GetCryptoCurrencyStatusSyncUseCase( suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean = false, ): Either { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, @@ -30,7 +31,7 @@ class GetCryptoCurrencyStatusSyncUseCase( networksRepository = networksRepository, ) - return operations.getCurrencyStatusSync(cryptoCurrencyId) + return operations.getCurrencyStatusSync(cryptoCurrencyId, isSingleWalletWithTokens) .mapLeft { error -> error.mapToCurrencyError() } } 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 5b3bf0649d..eb7296acea 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 @@ -1,13 +1,12 @@ package com.tangem.domain.tokens import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.* +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository @@ -57,25 +56,23 @@ class GetCurrencyWarningsUseCase( ), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)), + flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)), getSwapPromoNotificationWarning( operations = operations, userWalletId = userWalletId, currencyStatus = currencyStatus, ).conflate(), - ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeSwapPromo -> + ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource, maybeSwapPromo -> setOfNotNull( maybeSwapPromo, maybeRentWarning, - maybeEdWarning?.let { - CryptoCurrencyWarning.ExistentialDeposit( - currencyName = currency.name, - edStringValueWithSymbol = "${it.toPlainString()} ${currency.symbol}", - ) - }, - *coinRelatedWarnings.toTypedArray(), + maybeEdWarning?.let { getExistentialDepositWarning(currency, it) }, + maybeFeeResource?.let { getFeeResourceWarning(it) }, + * coinRelatedWarnings.toTypedArray(), getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), getBeaconChainShutdownWarning(currency.network.id), + getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), ) }.flowOn(dispatchers.io) } @@ -169,9 +166,6 @@ class GetCurrencyWarningsUseCase( when { tokenStatus != null && coinStatus != null -> { buildList { - if (currenciesRepository.hasPendingTransactions(tokenStatus, coinStatus)) { - add(CryptoCurrencyWarning.HasPendingTransactions(coinStatus.currency.symbol)) - } getFeeWarning( userWalletId = userWalletId, coinStatus = coinStatus, @@ -270,6 +264,39 @@ class GetCurrencyWarningsUseCase( return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null } + private fun getExistentialDepositWarning( + currency: CryptoCurrency, + amount: BigDecimal, + ): CryptoCurrencyWarning.ExistentialDeposit { + return CryptoCurrencyWarning.ExistentialDeposit( + currencyName = currency.name, + edStringValueWithSymbol = "${amount.toPlainString()} ${currency.symbol}", + ) + } + + private fun getFeeResourceWarning(feeResource: CurrencyAmount): CryptoCurrencyWarning.FeeResourceInfo { + return CryptoCurrencyWarning.FeeResourceInfo( + amount = feeResource.value, + maxAmount = feeResource.maxValue, + ) + } + + private suspend fun getAssetRequirementsWarning( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyWarning? { + return when (val requirements = walletManagersFacade.getAssetRequirements(userWalletId, currency)) { + is AssetRequirementsCondition.PaidTransaction -> HederaWarnings.AssociateWarning(currency = currency) + is AssetRequirementsCondition.PaidTransactionWithFee -> HederaWarnings.AssociateWarningWithFee( + currency = currency, + fee = requirements.feeAmount, + feeCurrencySymbol = requirements.feeCurrencySymbol, + feeCurrencyDecimals = requirements.decimals, + ) + null -> null + } + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } 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 cd41c82702..d38fa6a951 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 @@ -38,7 +38,9 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase( operations .getNetworkCoinSync(cryptoCurrency.network.id, cryptoCurrency.network.derivationPath) .getOrNull() - FeePaidCurrency.SameCurrency -> cryptoCurrencyStatus + FeePaidCurrency.SameCurrency, + is FeePaidCurrency.FeeResource, + -> cryptoCurrencyStatus is FeePaidCurrency.Token -> operations.getCurrencyStatusSync(feePaidCurrency.tokenId).getOrNull() } } 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 f33b882c8e..8b03aef79d 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 @@ -39,6 +39,26 @@ class GetNetworkCoinStatusUseCase( .flowOn(dispatchers.io) } + suspend fun invokeSync( + userWalletId: UserWalletId, + networkId: Network.ID, + derivationPath: Network.DerivationPath, + isSingleWalletWithTokens: Boolean, + ): Either { + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + ) + val maybeCurrency = if (isSingleWalletWithTokens) { + operations.getNetworkCoinForSingleWalletWithTokenSync(networkId) + } else { + operations.getNetworkCoinSync(networkId, derivationPath) + } + return maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) + } + private suspend fun getCurrency( userWalletId: UserWalletId, networkId: Network.ID, 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 1266f0cc23..afb4450d1c 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 @@ -1,37 +1,46 @@ package com.tangem.domain.tokens -import arrow.core.Either import arrow.core.left +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.EitherFlow +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.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.CurrenciesStatusesLceOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest class GetTokenListUseCase( - internal val currenciesRepository: CurrenciesRepository, - internal val quotesRepository: QuotesRepository, - internal val networksRepository: NetworksRepository, - internal val dispatchers: CoroutineDispatcherProvider, + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): Flow> { - return getTokensStatuses(userWalletId).transformLatest { maybeTokens -> + fun launch(userWalletId: UserWalletId): EitherFlow { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens -> maybeTokens.fold( ifLeft = { error -> - emit(error.left()) + emit(error.mapToTokenListError().left()) }, ifRight = { tokens -> emitAll(createTokenList(userWalletId, tokens)) @@ -40,32 +49,61 @@ class GetTokenListUseCase( } } - private fun getTokensStatuses( - userWalletId: UserWalletId, - ): Flow>> { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - useCase = this@GetTokenListUseCase, + @OptIn(ExperimentalCoroutinesApi::class) + fun launchLce(userWalletId: UserWalletId): LceFlow { + val operations = CurrenciesStatusesLceOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } + return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies -> + maybeCurrencies.fold( + ifLoading = { maybeContent -> + if (maybeContent != null) { + emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true)) + } else { + emit(lceLoading()) + } + }, + ifContent = { content -> + emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false)) + }, + ifError = { error -> emit(error.lceError()) }, + ) + } } private fun createTokenList( userWalletId: UserWalletId, tokens: List, - ): Flow> { + ): EitherFlow { val operations = TokenListOperations( userWalletId = userWalletId, tokens = tokens, - useCase = this@GetTokenListUseCase, + currenciesRepository = currenciesRepository, ) return operations.getTokenListFlow().map { maybeTokenList -> maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) } } + + private fun createTokenListLce( + userWalletId: UserWalletId, + currencies: List, + isCurrenciesLoading: Boolean, + ): LceFlow { + val operations = TokenListOperations( + userWalletId = userWalletId, + tokens = currencies, + currenciesRepository = currenciesRepository, + ) + + return operations.getTokenListFlow().map { maybeTokenList -> + maybeTokenList + .mapLeft(TokenListOperations.Error::mapToTokenListError) + .toLce(isCurrenciesLoading) + } + } } \ 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 new file mode 100644 index 0000000000..36da5cae5c --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -0,0 +1,82 @@ +package com.tangem.domain.tokens + +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lce +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.tokens.operations.CurrenciesStatusesLceOperations +import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.transform + +class GetWalletTotalBalanceUseCase( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, +) { + + suspend operator fun invoke( + userWallestIds: Collection, + ): LceFlow> { + val flows = userWallestIds.distinct() + .map { userWalletId -> + invoke(userWalletId).map { maybeBalance -> + userWalletId to maybeBalance + } + } + + return combine(flows) { balances -> + lce { + balances.associate { (userWalletId, maybeBalance) -> + userWalletId to maybeBalance.bind() + } + } + } + } + + suspend operator fun invoke(userWalletId: UserWalletId): LceFlow { + val currenciesStatuses = getStatuses(userWalletId) + + return currenciesStatuses.transform { maybeStatuses -> + val balance = createBalance(maybeStatuses) + + emit(balance) + } + } + + private fun createBalance( + maybeStatuses: Lce>, + ): Lce = lce { + val statuses = maybeStatuses.bind() + + val operations = TokenListFiatBalanceOperations( + currencies = ensureNotNull(statuses.toNonEmptyListOrNull()) { lceLoading() }, + isAnyTokenLoading = false, + ) + + operations.calculateFiatBalance() + } + + private fun getStatuses(userWalletId: UserWalletId): LceFlow> { + val operations = CurrenciesStatusesLceOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getCurrenciesStatuses( + userWalletId = userWalletId, + isSingleCurrencyWalletsAllowed = true, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt index d003a95067..59510b29f3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/IsAmountSubtractAvailableUseCase.kt @@ -23,6 +23,7 @@ class IsAmountSubtractAvailableUseCase( is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin is FeePaidCurrency.SameCurrency -> true is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId + is FeePaidCurrency.FeeResource -> false } } } 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 20f1ae77ef..a179238d94 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 @@ -8,6 +8,7 @@ import arrow.core.raise.withError 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.model.TotalFiatBalance import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -29,7 +30,7 @@ class ToggleTokenListGroupingUseCase( } private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { - ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { TokenListSortingError.TokenListIsLoading } @@ -47,7 +48,7 @@ class ToggleTokenListGroupingUseCase( private fun Raise.ungroupTokens( tokenList: TokenList.GroupedByNetwork, ): TokenList.Ungrouped { - ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { TokenListSortingError.TokenListIsLoading } 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 482940452a..2764f37e15 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 @@ -8,6 +8,7 @@ import arrow.core.raise.withError 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.model.TotalFiatBalance import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -31,7 +32,7 @@ class ToggleTokenListSortingUseCase( private fun Raise.sortGroupedTokenList( tokenList: TokenList.GroupedByNetwork, ): TokenList.GroupedByNetwork { - ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { TokenListSortingError.TokenListIsLoading } @@ -48,7 +49,7 @@ class ToggleTokenListSortingUseCase( private fun Raise.sortUngroupedTokenList( tokenList: TokenList.Ungrouped, ): TokenList.Ungrouped { - ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { TokenListSortingError.TokenListIsLoading } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CurrencyAmount.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CurrencyAmount.kt new file mode 100644 index 0000000000..e916aeee2e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CurrencyAmount.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +/** + * The amount of currency with the possible [maxValue] field + * Useful for some [com.tangem.blockchain.common.AmountType] that have maximum value + */ +data class CurrencyAmount( + val value: BigDecimal, + val maxValue: BigDecimal?, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt new file mode 100644 index 0000000000..54db16bf92 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens.model + +sealed class ScenarioUnavailabilityReason { + data object None : ScenarioUnavailabilityReason() + + // send&sell-specific + data class PendingTransaction( + val withdrawalScenario: WithdrawalScenario, + val networkName: String, + ) : ScenarioUnavailabilityReason() + data class EmptyBalance(val withdrawalScenario: WithdrawalScenario) : ScenarioUnavailabilityReason() + + // buy-specific + data class BuyUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + // swap-specific + data class NotExchangeable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + // sell-specific + data class NotSupportedBySellService(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + data object Unreachable : ScenarioUnavailabilityReason() + + data object UnassociatedAsset : ScenarioUnavailabilityReason() + + enum class WithdrawalScenario { + SELL, SEND + } +} \ 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 f52641b2c8..372f1e51d5 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 @@ -10,20 +10,20 @@ data class TokenActionsState( sealed class ActionState { - abstract val enabled: Boolean + abstract val unavailabilityReason: ScenarioUnavailabilityReason - data class Buy(override val enabled: Boolean) : ActionState() + data class Buy(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class CopyAddress(override val enabled: Boolean) : ActionState() + data class CopyAddress(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Sell(override val enabled: Boolean) : ActionState() + data class Sell(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Receive(override val enabled: Boolean) : ActionState() + data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Swap(override val enabled: Boolean) : ActionState() + data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Send(override val enabled: Boolean) : ActionState() + data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class HideToken(override val enabled: Boolean) : ActionState() + data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() } } \ No newline at end of file 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 index 089bbdefde..88c7af38bd 100644 --- 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 @@ -12,7 +12,7 @@ import java.math.BigDecimal * @property sortedBy The criteria used for sorting the tokens. */ sealed class TokenList { - open val totalFiatBalance: FiatBalance = FiatBalance.Loading + open val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loading open val sortedBy: SortType = SortType.NONE /** @@ -24,7 +24,7 @@ sealed class TokenList { */ data class GroupedByNetwork( val groups: List, - override val totalFiatBalance: FiatBalance, + override val totalFiatBalance: TotalFiatBalance, override val sortedBy: SortType, ) : TokenList() @@ -37,14 +37,14 @@ sealed class TokenList { */ data class Ungrouped( val currencies: List, - override val totalFiatBalance: FiatBalance, + override val totalFiatBalance: TotalFiatBalance, override val sortedBy: SortType, ) : TokenList() /** Represents a state where the token list is empty. */ object Empty : TokenList() { - override val totalFiatBalance: FiatBalance = FiatBalance.Loaded( + override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( amount = BigDecimal.ZERO, isAllAmountsSummarized = true, ) @@ -54,32 +54,4 @@ sealed class TokenList { enum class SortType { NONE, BALANCE, } - - /** - * Represents the possible states of the fiat balance, including loading, failure, or a loaded amount. - */ - sealed class FiatBalance { - /** - * Represents the loading state of the fiat balance. - * This state indicates that the fiat balance is currently being retrieved or calculated. - */ - object Loading : FiatBalance() - - /** - * Represents the failure state of the fiat balance. - * This state indicates that an attempt to retrieve or calculate the fiat balance has failed. - */ - object Failed : FiatBalance() - - /** - * Represents the successfully loaded state of the fiat balance. - * - * @property amount The loaded fiat balance amount. - * @property isAllAmountsSummarized Indicates whether the amount includes a summary of all underlying amounts. - */ - data class Loaded( - val amount: BigDecimal, - val isAllAmountsSummarized: Boolean, - ) : FiatBalance() - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt new file mode 100644 index 0000000000..98deb39ace --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +/** + * Represents the possible states of the fiat balance, including loading, failure, or a loaded amount. + */ +sealed class TotalFiatBalance { + /** + * Represents the loading state of the fiat balance. + * This state indicates that the fiat balance is currently being retrieved or calculated. + */ + 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. + */ + data object Failed : TotalFiatBalance() + + /** + * Represents the successfully loaded state of the fiat balance. + * + * @property amount The loaded fiat balance amount. + * @property isAllAmountsSummarized Indicates whether the amount includes a summary of all underlying amounts. + */ + data class Loaded( + val amount: BigDecimal, + val isAllAmountsSummarized: Boolean, + ) : TotalFiatBalance() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt new file mode 100644 index 0000000000..3dd2b0582b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -0,0 +1,182 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.* +import arrow.core.raise.recover +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lce +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +internal class CurrenciesStatusesLceOperations( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, +) { + + fun getCurrenciesStatuses( + userWalletId: UserWalletId, + isSingleCurrencyWalletsAllowed: Boolean = false, + ): LceFlow> { + return transformToCurrenciesStatuses( + userWalletId = userWalletId, + flow = if (isSingleCurrencyWalletsAllowed) { + getWalletCurrenies(userWalletId) + } else { + getMultiCurrencyWalletCurrencies(userWalletId) + }, + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun transformToCurrenciesStatuses( + userWalletId: UserWalletId, + flow: LceFlow>, + ): LceFlow> { + return flow.transformLatest transform@{ maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.fold( + ifLoading = { maybeContent -> + emit(createLoadingCurrenciesStatuses(maybeContent)) + return@transform + }, + ifContent = { content -> + val nonEmptyCurrencies = content.toNonEmptyListOrNull() + + if (nonEmptyCurrencies == null) { + emit(TokenListError.EmptyTokens.lceError()) + return@transform + } else { + nonEmptyCurrencies + } + }, + ifError = { error -> + emit(error.lceError()) + return@transform + }, + ) + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + + combine( + getQuotes(currenciesIds), + getNetworksStatuses(userWalletId, networks), + ) { maybeQuotes, maybeNetworksStatuses -> + val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + emit(statuses) + }.collect() + } + } + + private fun createLoadingCurrenciesStatuses( + maybeCurrencies: List?, + ): Lce> { + val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull() + + val statuses = if (nonEmptyCurrencies == null) { + lceLoading() + } else { + createCurrenciesStatuses( + nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + ) + } + + return statuses + } + + private fun getWalletCurrenies(userWalletId: UserWalletId): LceFlow> { + return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) + .map { maybeCurrencies -> + maybeCurrencies.mapError { TokenListError.DataError(it) } + } + } + + private fun getMultiCurrencyWalletCurrencies( + userWalletId: UserWalletId, + ): LceFlow> { + return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId) + .map { maybeCurrencies -> + maybeCurrencies.mapError { TokenListError.DataError(it) } + } + } + + private fun createCurrenciesStatuses( + currencies: NonEmptyList, + maybeQuotes: Either>?, + maybeNetworkStatuses: Lce>?, + ): Lce> = lce { + isLoading.set(maybeNetworkStatuses == null) + + var quotesRetrievingFailed = false + + val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull() + val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { + quotesRetrievingFailed = true + null + }?.ifEmpty { + quotesRetrievingFailed = true + null + } + + currencies.map { currency -> + val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + + createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + } + } + + private fun createCurrencyStatus( + currency: CryptoCurrency, + quote: Quote?, + networkStatus: NetworkStatus?, + ignoreQuote: Boolean, + ): CryptoCurrencyStatus { + val currencyStatusOperations = CurrencyStatusOperations( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = ignoreQuote, + ) + + return currencyStatusOperations.createTokenStatus() + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { + return quotesRepository.getQuotesUpdates(tokensIds) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + } + + private fun getNetworksStatuses( + userWalletId: UserWalletId, + networks: NonEmptySet, + ): LceFlow> { + return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks) + .map { maybeStatuses -> + maybeStatuses.mapError { TokenListError.DataError(it) } + } + } + + private fun getIds(currencies: List): Pair, NonEmptySet> { + val currencyIdToNetworkId = currencies.associate { currency -> + currency.id to currency.network + } + val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() + val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() + + requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } + requireNotNull(networks) { "Networks IDs cannot be empty" } + + return networks to currenciesIds + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index d12ca4c9ad..de1768de08 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* -import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -20,18 +20,8 @@ internal class CurrenciesStatusesOperations( private val userWalletId: UserWalletId, ) { - constructor( - userWalletId: UserWalletId, - useCase: GetTokenListUseCase, - ) : this( - currenciesRepository = useCase.currenciesRepository, - quotesRepository = useCase.quotesRepository, - networksRepository = useCase.networksRepository, - userWalletId = userWalletId, - ) - @OptIn(ExperimentalCoroutinesApi::class) - fun getCurrenciesStatusesFlow(): Flow>> { + fun getCurrenciesStatusesFlow(): EitherFlow> { return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> val nonEmptyCurrencies = maybeCurrencies.fold( ifLeft = { error -> @@ -87,12 +77,18 @@ internal class CurrenciesStatusesOperations( } } - suspend fun getCurrencyStatusSync(cryptoCurrencyId: CryptoCurrency.ID): Either { + suspend fun getCurrencyStatusSync( + cryptoCurrencyId: CryptoCurrency.ID, + isSingleWalletWithTokens: Boolean = false, + ): Either { return either { catch( block = { - val currency = + val currency = if (isSingleWalletWithTokens) { + currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, cryptoCurrencyId) + } else { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) + } val quotes = quotesRepository.getQuoteSync(cryptoCurrencyId).right() val networkStatuses = networksRepository.getNetworkStatusesSync( @@ -121,6 +117,14 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusSync(currency.id) } + suspend fun getNetworkCoinForSingleWalletWithTokenSync( + networkId: Network.ID, + ): Either = either { + val currency = getNetworkCoinForSingleWalletWithToken(networkId) + + return getCurrencyStatusSync(currency.id) + } + suspend fun getPrimaryCurrencyStatusSync(): Either = either { val currency = catch( block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, 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 b02828d2c3..8284d1667e 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 @@ -12,7 +12,7 @@ internal class CurrencyStatusOperations( fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) - private fun createStatus(): CryptoCurrencyStatus.Status { + private fun createStatus(): CryptoCurrencyStatus.Value { return when (val status = networkStatus?.value) { null -> CryptoCurrencyStatus.Loading is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() @@ -42,7 +42,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, ) - private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { + private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Value { val amount = when (val amount = status.amounts[currency.id]) { null -> { return CryptoCurrencyStatus.Loading 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 78db26e8a0..2ff01885c9 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 @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import java.math.BigDecimal internal class TokenListFiatBalanceOperations( @@ -10,14 +10,14 @@ internal class TokenListFiatBalanceOperations( private val isAnyTokenLoading: Boolean, ) { - fun calculateFiatBalance(): TokenList.FiatBalance { - var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading + fun calculateFiatBalance(): TotalFiatBalance { + var fiatBalance: TotalFiatBalance = TotalFiatBalance.Loading if (isAnyTokenLoading) return fiatBalance for (token in currencies) { when (val status = token.value) { is CryptoCurrencyStatus.Loading -> { - fiatBalance = TokenList.FiatBalance.Loading + fiatBalance = TotalFiatBalance.Loading break } is CryptoCurrencyStatus.MissedDerivation, @@ -25,7 +25,7 @@ internal class TokenListFiatBalanceOperations( is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.NoQuote, -> { - fiatBalance = TokenList.FiatBalance.Failed + fiatBalance = TotalFiatBalance.Failed break } is CryptoCurrencyStatus.NoAccount -> { @@ -43,9 +43,9 @@ internal class TokenListFiatBalanceOperations( return fiatBalance } - private fun recalculateNoAccountBalance(currentBalance: TokenList.FiatBalance): TokenList.FiatBalance { - return (currentBalance as? TokenList.FiatBalance.Loaded)?.copy(isAllAmountsSummarized = false) - ?: TokenList.FiatBalance.Loaded( + private fun recalculateNoAccountBalance(currentBalance: TotalFiatBalance): TotalFiatBalance { + return (currentBalance as? TotalFiatBalance.Loaded)?.copy(isAllAmountsSummarized = false) + ?: TotalFiatBalance.Loaded( amount = BigDecimal.ZERO, isAllAmountsSummarized = false, ) @@ -53,12 +53,12 @@ internal class TokenListFiatBalanceOperations( private fun recalculateBalance( status: CryptoCurrencyStatus.Loaded, - currentBalance: TokenList.FiatBalance, - ): TokenList.FiatBalance { + currentBalance: TotalFiatBalance, + ): TotalFiatBalance { return with(currentBalance) { - (this as? TokenList.FiatBalance.Loaded)?.copy( + (this as? TotalFiatBalance.Loaded)?.copy( amount = this.amount + status.fiatAmount, - ) ?: TokenList.FiatBalance.Loaded( + ) ?: TotalFiatBalance.Loaded( amount = status.fiatAmount, isAllAmountsSummarized = true, ) @@ -67,15 +67,15 @@ internal class TokenListFiatBalanceOperations( private fun recalculateBalance( status: CryptoCurrencyStatus.Custom, - currentBalance: TokenList.FiatBalance, - ): TokenList.FiatBalance { + currentBalance: TotalFiatBalance, + ): TotalFiatBalance { return with(currentBalance) { val isTokenAmountCanBeSummarized = status.fiatAmount != null - (this as? TokenList.FiatBalance.Loaded)?.copy( + (this as? TotalFiatBalance.Loaded)?.copy( amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO), isAllAmountsSummarized = isTokenAmountCanBeSummarized, - ) ?: TokenList.FiatBalance.Loaded( + ) ?: TotalFiatBalance.Loaded( amount = status.fiatAmount ?: BigDecimal.ZERO, isAllAmountsSummarized = isTokenAmountCanBeSummarized, ) 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 612140fbfb..0af48b7898 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 @@ -4,9 +4,9 @@ import arrow.core.* import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.withError -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.* @@ -18,16 +18,6 @@ internal class TokenListOperations( private val tokens: List, ) { - constructor( - userWalletId: UserWalletId, - tokens: List, - useCase: GetTokenListUseCase, - ) : this( - currenciesRepository = useCase.currenciesRepository, - userWalletId = userWalletId, - tokens = tokens, - ) - fun getTokenListFlow(): Flow> { return combine( getIsGrouped(), @@ -82,7 +72,7 @@ internal class TokenListOperations( private fun Raise.createTokenList( currencies: NonEmptyList, - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, isAnyTokenLoading: Boolean, isGrouped: Boolean, isSortedByBalance: Boolean, @@ -98,7 +88,7 @@ internal class TokenListOperations( private fun Raise.createTokenList( sortingOperations: TokenListSortingOperations, - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, isGrouped: Boolean, ): TokenList { return if (isGrouped) { @@ -110,7 +100,7 @@ internal class TokenListOperations( private fun Raise.createUngroupedTokenList( sortingOperations: TokenListSortingOperations, - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, ): TokenList.Ungrouped = TokenList.Ungrouped( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, @@ -124,7 +114,7 @@ internal class TokenListOperations( private fun Raise.createGroupedTokenList( sortingOperations: TokenListSortingOperations, - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, ): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork( sortedBy = sortingOperations.getSortType(), totalFiatBalance = fiatBalance, @@ -138,7 +128,7 @@ internal class TokenListOperations( private fun createUnsortedUngroupedTokenList( tokens: List, - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, ): TokenList.Ungrouped { return TokenList.Ungrouped( sortedBy = TokenList.SortType.NONE, 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 2833be87eb..354252452c 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 @@ -7,10 +7,7 @@ import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.* import java.math.BigDecimal internal class TokenListSortingOperations( @@ -22,7 +19,7 @@ internal class TokenListSortingOperations( constructor( tokenList: TokenList, sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, - isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TokenList.FiatBalance.Loading, + isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TotalFiatBalance.Loading, ) : this( currencies = when (tokenList) { is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } 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 60f8d5da7e..5bdf7b4067 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 @@ -1,5 +1,7 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.core.error.DataError +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -20,7 +22,7 @@ interface CurrenciesRepository { * @param currencies The list of cryptocurrencies to be saved. * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun saveTokens( @@ -35,7 +37,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param currencies The currencies which must be added. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) @@ -45,7 +47,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param currency The currency which must be removed. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) @@ -55,17 +57,28 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param currencies The currencies which must be removed. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) + /** + * Retrieves the list of cryptocurrencies within a user wallet. + * + * This method returns a list of cryptocurrencies associated with the user wallet regardless of whether + * it is a multi-currency or single-currency wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A list of [CryptoCurrency]. + */ + fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> + /** * Retrieves the primary cryptocurrency for a specific single-currency user wallet. * * @param userWalletId The unique identifier of the user wallet. * @return The primary cryptocurrency associated with the user wallet. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency @@ -75,7 +88,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return The primary cryptocurrency associated with the user wallet. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List @@ -87,7 +100,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. * @return The cryptocurrency associated with the user wallet and ID. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun getSingleCurrencyWalletWithCardCurrency( @@ -102,11 +115,22 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> + /** + * Retrieves updates of the list of cryptocurrencies within a multi-currency wallet. + * + * Loads remote cryptocurrencies if they have expired. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an + * [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided. + */ + fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow> + /** * Retrieves the list of cryptocurrencies within a multi-currency wallet. * @@ -115,7 +139,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A list of [CryptoCurrency]. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun getMultiCurrencyWalletCurrenciesSync( @@ -129,7 +153,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. * @return The cryptocurrency associated with the user wallet and ID. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency @@ -152,7 +176,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ fun isTokensGrouped(userWalletId: UserWalletId): Flow @@ -162,7 +186,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow @@ -181,4 +205,9 @@ interface CurrenciesRepository { * Retrieves fee paid currency for specific [currency]. */ suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency + + /** + * Creates token [cryptoCurrency] based on current token and [network] it`s will be added + */ + fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token } \ No newline at end of file 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 914007040a..0b5745a983 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,5 +1,6 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.wallets.models.UserWalletId @@ -19,6 +20,12 @@ interface CurrencyChecksRepository { /** Returns reserve amount which is required to create an account */ suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? + /** Returns a fee resource amount available and max for paying fees in several blockchains */ + suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? + + /** Is fee resource enough for a transaction amount */ + suspend fun checkIfFeeResourceEnough(amount: BigDecimal, userWalletId: UserWalletId, network: Network): Boolean + /** Returns true if account with [address] was reserved with minimum amount */ suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 271cdb4fcf..a2033bb707 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus @@ -21,6 +22,20 @@ interface NetworksRepository { */ fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + /** + * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. + * + * Loads remote network statuses if they have expired. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networks A set of network which statuses are to be retrieved. + * @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. + */ + fun getNetworkStatusesUpdatesLce( + userWalletId: UserWalletId, + networks: Set, + ): LceFlow> + /** * Fetches pending transactions for given network * @@ -47,5 +62,8 @@ interface NetworksRepository { fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean + /** + * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] + */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 4c38eda580..64a0d29a12 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -30,5 +30,5 @@ interface QuotesRepository { */ suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set - suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote + suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 68413e7f65..613246a85c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -17,7 +17,6 @@ import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -27,7 +26,6 @@ import org.junit.Test internal class GetTokenListUseCaseTest { - private val dispatchers = TestingCoroutineDispatcherProvider() private val userWalletId = UserWalletId(value = null) @Ignore @@ -45,7 +43,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -61,7 +59,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -81,7 +79,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -97,7 +95,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -111,7 +109,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left())) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -136,7 +134,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 3) .toList() @@ -155,7 +153,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(isGrouped = flowOf(true.right())) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -177,7 +175,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -199,7 +197,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -214,7 +212,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(tokens = flowOf(emptyList().right())) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -227,7 +225,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(tokens = flowOf()) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -243,7 +241,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(statuses = flowOf()) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -258,7 +256,7 @@ internal class GetTokenListUseCaseTest { val useCase = getUseCase(statuses = flowOf(emptySet().right())) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -277,7 +275,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId) + val result = useCase.launch(userWalletId) .take(count = 2) .toList() @@ -295,7 +293,7 @@ internal class GetTokenListUseCaseTest { ) // When - val result = useCase(userWalletId).first() + val result = useCase.launch(userWalletId).first() // Then assertEquals(expectedResult, result) @@ -308,7 +306,6 @@ internal class GetTokenListUseCaseTest { isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), ) = GetTokenListUseCase( - dispatchers = dispatchers, currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), removeCurrencyResult = Unit.right(), 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 0f03851309..091154dda9 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 @@ -21,6 +21,7 @@ internal object MockNetworks { backendId = "network1", currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, + hasFiatFeeRate = true, ) val network2 = Network( @@ -31,6 +32,7 @@ internal object MockNetworks { backendId = "network1", currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, + hasFiatFeeRate = true, ) val network3 = Network( @@ -41,6 +43,7 @@ internal object MockNetworks { backendId = "network1", currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, + hasFiatFeeRate = true, ) val networkStatus1 = NetworkStatus( 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 b831c9595a..e08a5348dd 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 @@ -7,6 +7,7 @@ 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 com.tangem.domain.tokens.model.TotalFiatBalance import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") @@ -19,43 +20,43 @@ internal object MockTokenLists { val emptyGroupedTokenList = TokenList.GroupedByNetwork( groups = emptyList(), - totalFiatBalance = TokenList.FiatBalance.Failed, + totalFiatBalance = TotalFiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) val emptyUngroupedTokenList = TokenList.Ungrouped( currencies = emptyList(), - totalFiatBalance = TokenList.FiatBalance.Failed, + totalFiatBalance = TotalFiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) val failedGroupedTokenList = TokenList.GroupedByNetwork( groups = failedNetworksGroups, - totalFiatBalance = TokenList.FiatBalance.Failed, + totalFiatBalance = TotalFiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) val failedUngroupedTokenList = TokenList.Ungrouped( currencies = MockTokensStates.failedTokenStates, - totalFiatBalance = TokenList.FiatBalance.Failed, + totalFiatBalance = TotalFiatBalance.Failed, sortedBy = TokenList.SortType.NONE, ) val noQuotesUngroupedTokenList = failedUngroupedTokenList.copy( - totalFiatBalance = TokenList.FiatBalance.Failed, + totalFiatBalance = TotalFiatBalance.Failed, currencies = MockTokensStates.noQuotesTokensStatuses, ) val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() ?: emptyList(), - totalFiatBalance = TokenList.FiatBalance.Loading, + totalFiatBalance = TotalFiatBalance.Loading, ) } val loadingGroupedTokenList = with(failedGroupedTokenList) { copy( - totalFiatBalance = TokenList.FiatBalance.Loading, + totalFiatBalance = TotalFiatBalance.Loading, groups = groups.map { group -> group.copy( currencies = group.currencies @@ -72,7 +73,7 @@ internal object MockTokenLists { return failedUngroupedTokenList.copy( currencies = tokens, sortedBy = TokenList.SortType.NONE, - totalFiatBalance = TokenList.FiatBalance.Loaded( + totalFiatBalance = TotalFiatBalance.Loaded( amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, ), @@ -86,7 +87,7 @@ internal object MockTokenLists { return failedGroupedTokenList.copy( groups = groups, sortedBy = TokenList.SortType.NONE, - totalFiatBalance = TokenList.FiatBalance.Loaded( + totalFiatBalance = TotalFiatBalance.Loaded( amount = groups .flatMap { it.currencies as NonEmptyList } .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, 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 6fb77065d5..d4d616d88a 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 @@ -3,6 +3,8 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -52,6 +54,10 @@ internal class MockCurrenciesRepository( override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit + override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> { + return emptyFlow() + } + override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, @@ -78,6 +84,12 @@ internal class MockCurrenciesRepository( return tokens.map { it.getOrElse { e -> throw e } } } + override fun getMultiCurrencyWalletCurrenciesUpdatesLce( + userWalletId: UserWalletId, + ): LceFlow> { + return tokens.map { it.toLce() } + } + override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, @@ -120,4 +132,8 @@ internal class MockCurrenciesRepository( override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency { return FeePaidCurrency.Coin } + + override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { + return cryptoCurrency + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 34abccfa04..78e06e7087 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -4,6 +4,8 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId @@ -22,6 +24,13 @@ internal class MockNetworksRepository( return statuses.map { it.getOrElse { e -> throw e } } } + override fun getNetworkStatusesUpdatesLce( + userWalletId: UserWalletId, + networks: Set, + ): LceFlow> { + return statuses.map { it.toLce() } + } + override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { // no-op } diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index f0345df888..d4014c6f47 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/domain/transaction/models/.gitignore b/domain/transaction/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/transaction/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/transaction/models/build.gradle.kts b/domain/transaction/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/transaction/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt new file mode 100644 index 0000000000..3503f354df --- /dev/null +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.transaction.models + +import java.math.BigDecimal + +sealed class AssetRequirementsCondition { + + /** + * The exact value of the fee for this type of condition is unknown. + */ + data object PaidTransaction : AssetRequirementsCondition() + + /** + * The exact value of the fee for this type of condition is stored in `feeAmount`. + */ + data class PaidTransactionWithFee( + val feeAmount: BigDecimal, + val feeCurrencySymbol: String, + val decimals: Int, + ) : AssetRequirementsCondition() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 70c8b7d0ae..52b707e8d1 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -1,10 +1,8 @@ package com.tangem.domain.transaction -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.CommonSigner -import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -18,12 +16,28 @@ interface TransactionRepository { destination: String, userWalletId: UserWalletId, network: Network, + isSwap: Boolean, + txExtras: TransactionExtras?, + hash: String?, ): TransactionData? + @Suppress("LongParameterList") + suspend fun validateTransaction( + amount: Amount, + fee: Fee?, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean = false, + txExtras: TransactionExtras?, + hash: String? = null, + ): Result + suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, userWalletId: UserWalletId, network: Network, - ): SimpleResult + ): com.tangem.blockchain.extensions.Result } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt similarity index 91% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt rename to domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index 624ad44419..8d09e3b248 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.wallets.repository +package com.tangem.domain.transaction import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.ParsedQrCode diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt new file mode 100644 index 0000000000..fc92dd8cea --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.transaction.error + +import com.tangem.domain.tokens.model.CryptoCurrency + +sealed class AssociateAssetError { + data class NotEnoughBalance(val feeCurrency: CryptoCurrency) : AssociateAssetError() + + data class DataError(val message: String?) : AssociateAssetError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt new file mode 100644 index 0000000000..75262c24e2 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.transaction.error + +sealed class ValidateAddressError { + data object AddressInWallet : ValidateAddressError() + data object InvalidAddress : ValidateAddressError() + data class DataError(val throwable: Throwable) : ValidateAddressError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt new file mode 100644 index 0000000000..72ad169dba --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.transaction.error.AssociateAssetError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.isNullOrZero + +class AssociateAssetUseCase( + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Either { + return either { + val networkCoin = currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = currency.network.id, + derivationPath = currency.network.derivationPath, + ) + if (isBalanceZero(userWalletId, networkCoin)) { + raise(AssociateAssetError.NotEnoughBalance(networkCoin)) + } + val signer = cardSdkConfigRepository.getCommonSigner(cardId = null) + + catch( + block = { + when (val result = walletManagersFacade.associateAsset(userWalletId, currency, signer)) { + is SimpleResult.Failure -> raise(AssociateAssetError.DataError(result.error.customMessage)) + SimpleResult.Success -> Unit + } + }, + catch = { error -> AssociateAssetError.DataError(error.message) }, + ) + } + } + + private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { + val networkStatus = networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = setOf(currency.network), + ).find { it.network == currency.network } + val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified) + ?.amounts + ?.get(currency.id) + return networkCoinAmountStatus is CryptoCurrencyAmountStatus.Loaded && + networkCoinAmountStatus.value.isNullOrZero() + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 55c5e0cd88..056dfc0f0c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository @@ -22,6 +23,9 @@ class CreateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, + txExtras: TransactionExtras? = null, + isSwap: Boolean = false, + hash: String? = null, ) = Either.catch { requireNotNull( transactionRepository.createTransaction( @@ -31,6 +35,9 @@ class CreateTransactionUseCase( destination = destination, userWalletId = userWalletId, network = network, + isSwap = isSwap, + txExtras = txExtras, + hash = hash, ), ) { "Failed to create transaction" } } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsUtxoConsolidationAvailableUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsUtxoConsolidationAvailableUseCase.kt new file mode 100644 index 0000000000..90839203b0 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/IsUtxoConsolidationAvailableUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Gets UTXO consolidation availability + */ +class IsUtxoConsolidationAvailableUseCase( + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend fun invokeSync(userWalletId: UserWalletId, network: Network) = + walletManagersFacade.checkUtxoConsolidationAvailability( + userWalletId = userWalletId, + network = network, + ) +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ParseSharedAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ParseSharedAddressUseCase.kt similarity index 85% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ParseSharedAddressUseCase.kt rename to domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ParseSharedAddressUseCase.kt index 8d03b6ab6d..eb5f1e1c89 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ParseSharedAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ParseSharedAddressUseCase.kt @@ -1,9 +1,9 @@ -package com.tangem.domain.wallets.usecase +package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.wallets.models.ParsedQrCode -import com.tangem.domain.wallets.repository.WalletAddressServiceRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 379d90d4be..faccbea7e4 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -6,7 +6,8 @@ import arrow.core.right import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSigner -import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.common.transaction.TransactionSendResult +import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.extensions.resourceReference @@ -35,7 +36,7 @@ class SendTransactionUseCase( txData: TransactionData, userWallet: UserWallet, network: Network, - ): Either { + ): Either { val signer = cardSdkConfigRepository.getCommonSigner(cardId = null) val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() @@ -51,12 +52,16 @@ class SendTransactionUseCase( signer = signer, ) } else { - transactionRepository.sendTransaction( + val sendResult = transactionRepository.sendTransaction( txData = txData, signer = signer, userWalletId = userWallet.walletId, network = network, - ).right() + ) + when (sendResult) { + is Result.Failure -> handleError(sendResult).left() + is Result.Success -> sendResult.data.right() + } } } catch (ex: Exception) { cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) @@ -65,12 +70,7 @@ class SendTransactionUseCase( cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) return sendResult.fold( - ifRight = { result -> - when (result) { - is SimpleResult.Success -> true.right() - is SimpleResult.Failure -> handleError(result).left() - } - }, + ifRight = { result -> result.hash.right() }, ifLeft = { it.left() }, ) } @@ -80,7 +80,7 @@ class SendTransactionUseCase( network: Network, transactionData: TransactionData, signer: TransactionSigner, - ): Either { + ): Either { val demoTransactionSender = DemoTransactionSender( walletManagersFacade .getOrCreateWalletManager(userWallet.walletId, network) @@ -89,14 +89,14 @@ class SendTransactionUseCase( val result = demoTransactionSender.send(transactionData = transactionData, signer = signer) - return if (result is SimpleResult.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) { + return if (result is Result.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) { SendTransactionError.DemoCardError.left() } else { - result.right() + TransactionSendResult("hash").right() } } - private fun handleError(result: SimpleResult.Failure): SendTransactionError { + private fun handleError(result: Result.Failure): SendTransactionError { if (ResultChecker.isNetworkError(result)) { return SendTransactionError.NetworkError( code = result.error.message, 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 new file mode 100644 index 0000000000..8be00045ba --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId + +class ValidateTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + @Suppress("LongParameterList") + suspend operator fun invoke( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean = false, + hash: String? = null, + ): Either { + return transactionRepository.validateTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + isSwap = isSwap, + txExtras = null, + hash = hash, + ) + .fold(onSuccess = { Unit.right() }, onFailure = { it.left() }) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt new file mode 100644 index 0000000000..7db3a825f7 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for validating wallet address. + */ +class ValidateWalletAddressUseCase( + private val walletAddressServiceRepository: WalletAddressServiceRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + address: String, + currencyAddress: Set?, + ): Either { + val isUtxoConsolidationAvailable = + walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) + val isCurrentAddress = currencyAddress?.any { it.value == address } ?: true + + val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable + val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, address) + + return when { + !isValidAddress -> ValidateAddressError.InvalidAddress.left() + isForbidSelfSend -> ValidateAddressError.AddressInWallet.left() + else -> Unit.right() + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt similarity index 77% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt rename to domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt index 754819852b..d4d5ab22e8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletMemoUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt @@ -1,8 +1,8 @@ -package com.tangem.domain.wallets.usecase +package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.repository.WalletAddressServiceRepository +import com.tangem.domain.transaction.WalletAddressServiceRepository /** * Use case for validating wallet memo. diff --git a/domain/wallet-connect/.gitignore b/domain/wallet-connect/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/wallet-connect/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts new file mode 100644 index 0000000000..af72640fab --- /dev/null +++ b/domain/wallet-connect/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + /* Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/CheckIsWalletConnectAvailableUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/CheckIsWalletConnectAvailableUseCase.kt new file mode 100644 index 0000000000..698fc71f7c --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/CheckIsWalletConnectAvailableUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.walletconnect + +import arrow.core.Either +import com.tangem.domain.walletconnect.repository.WalletConnectRepository +import com.tangem.domain.wallets.models.UserWalletId + +class CheckIsWalletConnectAvailableUseCase( + private val walletConnectRepository: WalletConnectRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + return Either.catch { walletConnectRepository.checkIsAvailable(userWalletId) } + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WalletConnectRepository.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WalletConnectRepository.kt new file mode 100644 index 0000000000..8e2605d780 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WalletConnectRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.walletconnect.repository + +import com.tangem.domain.wallets.models.UserWalletId + +interface WalletConnectRepository { + + suspend fun checkIsAvailable(userWalletId: UserWalletId): Boolean +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 0f3adc92e4..bd175a01fa 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { // region Domain modules implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt similarity index 69% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt index 325fb2efa7..ffa69e47ae 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt @@ -1,36 +1,22 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.builder import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase class UserWalletBuilder( private val scanResponse: ScanResponse, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(), ) { private var backupCardsIds: Set = emptySet() private var hasBackupError: Boolean = false private val CardDTO.isBackupNotAllowed: Boolean - get() = !this.settings.isBackupAllowed - - private val ScanResponse.userWalletName: String - get() = when (productType) { - ProductType.Note -> "Note" - ProductType.Twins -> "Twin" - ProductType.Start2Coin -> "Start2Coin" - ProductType.Visa -> "Tangem Visa" - ProductType.Wallet, - ProductType.Wallet2, - ProductType.Ring, - -> when { - card.isBackupNotAllowed -> "Tangem card" - cardTypesResolver.isStart2Coin() -> "Start2Coin" - else -> "Wallet" - } - } + get() = !settings.isBackupAllowed /** * DANGEROUS!!! @@ -56,7 +42,11 @@ class UserWalletBuilder( ?.let { UserWallet( walletId = it, - name = userWalletName, + name = generateWalletNameUseCase( + productType = productType, + isBackupNotAllowed = card.isBackupNotAllowed, + isStartToCoin = cardTypesResolver.isStart2Coin(), + ), artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletIdBuilder.kt similarity index 98% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletIdBuilder.kt index 607484d8a8..7c916ecec1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletIdBuilder.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.builder import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.hexToBytes diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UseCaseUtils.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UseCaseUtils.kt deleted file mode 100644 index ee7de756db..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UseCaseUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import arrow.core.raise.Raise -import arrow.core.raise.ensureNotNull - -internal inline fun Raise.ensureUserWalletListManagerNotNull( - walletsStateHolder: WalletsStateHolder, - raise: (Throwable) -> Error, -): UserWalletsListManager { - return ensureNotNull( - value = walletsStateHolder.userWalletsListManager, - raise = { - raise(IllegalStateException("User wallets list manager not initialized")) - }, - ) -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index 213d780d07..9b63847e73 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -7,12 +7,20 @@ import kotlinx.coroutines.flow.Flow interface UserWalletsListManager { + /** + * Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable] + * */ + val isLockable: Boolean + /** [Flow] with all saved [UserWallet]s updates */ val userWallets: Flow> /** [Flow] with selected [UserWallet] updates */ val selectedUserWallet: Flow + /** [List] with all saved [UserWallet]s updates */ + val userWalletsSync: List + /** Selected [UserWallet] */ val selectedUserWalletSync: UserWallet? @@ -84,11 +92,6 @@ interface UserWalletsListManager { */ suspend fun get(userWalletId: UserWalletId): CompletionResult - /** - * Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable] - * */ - fun isLockable(): Boolean - interface Lockable : UserWalletsListManager { /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt index a6bdbde07f..9570eaa90d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt @@ -49,7 +49,7 @@ suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockTyp * [UserWalletsListManager.Lockable] otherwise * */ fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? { - if (this.isLockable()) { + if (this.isLockable) { return this as? UserWalletsListManager.Lockable } return null diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt deleted file mode 100644 index d5af7e4449..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.wallets.legacy - -interface UserWalletsListManagerFeatureToggles { - - val isGeneralManagerEnabled: Boolean -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/WalletsStateHolder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/WalletsStateHolder.kt deleted file mode 100644 index 7aa875c6af..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/WalletsStateHolder.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import kotlinx.coroutines.flow.Flow - -interface WalletsStateHolder { - - val userWalletsListManager: UserWalletsListManager? - - val userWalletListManagerFlow: Flow -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/Artwork.kt similarity index 66% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/models/Artwork.kt index d788aed532..00b7961d48 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/Artwork.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.models data class Artwork(val artworkId: String) { @@ -6,9 +6,9 @@ data class Artwork(val artworkId: String) { const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png" const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png" + const val TWIN_CARD_1_URL = "https://app.tangem.com/cards/card_tg085.png" + const val TWIN_CARD_2_URL = "https://app.tangem.com/cards/card_tg086.png" const val SERGIO_CARD_ID = "BC01" const val MARTA_CARD_ID = "BC02" - const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" - const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt index 29669fa19e..d58c220ac3 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt @@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models sealed interface DeleteWalletError { - object DataError : DeleteWalletError - - object UnableToDelete : DeleteWalletError + data object UnableToDelete : DeleteWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt index 50d1bea262..3e7e62a562 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt @@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models sealed class GetUserWalletError { - data class DataError(val cause: Throwable) : GetUserWalletError() - - object UserWalletNotFound : GetUserWalletError() + data object UserWalletNotFound : GetUserWalletError() } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt index 7b320009c7..a41524b8b7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt @@ -5,7 +5,9 @@ package com.tangem.domain.wallets.models */ sealed interface SaveWalletError { - object DataError : SaveWalletError + val messageId: Int? - data class WalletAlreadySaved(val messageId: Int) : SaveWalletError + data class DataError(override val messageId: Int?) : SaveWalletError + + data class WalletAlreadySaved(override val messageId: Int) : SaveWalletError } \ 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 index a357f06ca9..e2aeab608f 100644 --- 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 @@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models sealed interface SelectWalletError { - object DataError : SelectWalletError - object UnableToSelectUserWallet : SelectWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt index 48a93dd33b..b15ec332d1 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt @@ -2,5 +2,7 @@ package com.tangem.domain.wallets.models sealed interface UpdateWalletError { - object DataError : UpdateWalletError + data object DataError : UpdateWalletError + + data object NameAlreadyExists : UpdateWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletNamesMigrationRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletNamesMigrationRepository.kt new file mode 100644 index 0000000000..0fa502f042 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletNamesMigrationRepository.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.wallets.repository + +/** + * Access to migrate names flag + */ +interface WalletNamesMigrationRepository { + + suspend fun isMigrationDone(): Boolean + + suspend fun setMigrationDone() +} \ No newline at end of file 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 2b43096bf4..b4db11932f 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 @@ -1,37 +1,36 @@ package com.tangem.domain.wallets.usecase 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.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.DeleteWalletError import com.tangem.domain.wallets.models.UserWalletId /** - * Use case for updating user wallet + * Use case for deleting user wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class DeleteWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { - suspend operator fun invoke(userWalletId: UserWalletId): Either { + /** + * Deletes user wallet with provided ID. + * + * @param userWalletId ID of user wallet to be deleted. + * + * @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. + * */ + suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { DeleteWalletError.DataError }, - ) - userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) - .doOnSuccess { return Unit.right() } - .doOnFailure { return DeleteWalletError.UnableToDelete.left() } + .doOnFailure { + raise(DeleteWalletError.UnableToDelete) + } - return Unit.right() + userWalletsListManager.hasUserWallets } } } \ 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 new file mode 100644 index 0000000000..f1d1852f0c --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.wallets.legacy.UserWalletsListManager + +/** + * Use case for user wallet name generation + */ +class GenerateWalletNameUseCase( + private val userWalletsListManager: UserWalletsListManager, +) { + + operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { + val defaultName = getDefaultName( + productType = productType, + isBackupNotAllowed = isBackupNotAllowed, + isStartToCoin = isStartToCoin, + ) + + val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + return suggestedWalletName(defaultName, existingNames) + } + + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { + val startIndex = 2 + if (!existingNames.contains(defaultName)) { + return defaultName + } + + for (index in startIndex..MAX_WALLETS_LIMIT) { + val potentialName = "$defaultName $index" + if (!existingNames.contains(potentialName)) { + return potentialName + } + } + + return defaultName + } + + private fun getDefaultName(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { + return when (productType) { + ProductType.Note -> "Note" + ProductType.Twins -> "Twin" + ProductType.Start2Coin -> "Start2Coin" + ProductType.Visa -> "Tangem Visa" + ProductType.Wallet, + ProductType.Wallet2, + ProductType.Ring, + -> when { + isBackupNotAllowed -> "Tangem card" + isStartToCoin -> "Start2Coin" + else -> "Wallet" + } + } + } + + companion object { + const val MAX_WALLETS_LIMIT = 10000 + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt similarity index 89% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index afa5eaaa37..3d92875a5b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -1,9 +1,10 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.usecase import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper +import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.attestation.TangemApi @@ -42,8 +43,8 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL else -> when (TwinsHelper.getTwinCardNumber(cardId)) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1 - TwinCardNumber.Second -> Artwork.TWIN_CARD_2 + TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL + TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL else -> Artwork.DEFAULT_IMG_URL } } 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 7cd6ffa8a2..e9f51327a6 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 @@ -3,8 +3,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet @@ -12,19 +11,14 @@ import com.tangem.domain.wallets.models.UserWallet * Use case for getting selected wallet. * Important! If all wallets is locked, use case returns a error. * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class GetSelectedWalletSyncUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { operator fun invoke(): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = GetUserWalletError::DataError, - ) - ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, raise = { GetUserWalletError.UserWalletNotFound }, 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 ee23723206..bda3c953ee 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 @@ -2,8 +2,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow @@ -11,19 +10,14 @@ import kotlinx.coroutines.flow.Flow /** * Use case for getting flow of selected wallet. * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class GetSelectedWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { operator fun invoke(): Either> { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = GetUserWalletError::DataError, - ) - userWalletsListManager.selectedUserWallet } } 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 2b9c40ae27..9323fd5423 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 @@ -3,22 +3,15 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.firstOrNull -class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { - suspend operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = GetUserWalletError::DataError, - ) - - val userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty() + operator fun invoke(userWalletId: UserWalletId): Either = either { + val userWallets = userWalletsListManager.userWalletsSync ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) 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 new file mode 100644 index 0000000000..0108e03b67 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.wallets.legacy.UserWalletsListManager + +/** + * Use case for getting list of user wallets names. + * + * @property userWalletsListManager user wallets list manager + */ +class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) { + + operator fun invoke(): List = 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 e40e284270..554791e869 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,23 +1,21 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull /** * Use case for getting list of user wallets * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class GetWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = - requireNotNull(walletsStateHolder.userWalletsListManager).userWallets + operator fun invoke(): Flow> = userWalletsListManager.userWallets @Throws(IllegalArgumentException::class) - suspend fun invokeSync(): List? = walletsStateHolder.userWalletsListManager?.userWallets?.firstOrNull() + fun invokeSync(): List = userWalletsListManager.userWalletsSync } \ No newline at end of file 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 24fdc5a232..e403101dd5 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 @@ -1,19 +1,19 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** * Use case that checks if wallet need backup cards + * + * @property userWalletsListManager user wallets list manager */ -class IsNeedToBackupUseCase(private val walletsStateHolder: WalletsStateHolder) { +class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) { operator fun invoke(id: UserWalletId): Flow { - val userWalletsListManager = requireNotNull(walletsStateHolder.userWalletsListManager) - return userWalletsListManager.userWallets .map { wallets -> val wallet = wallets.firstOrNull { it.walletId == id } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt new file mode 100644 index 0000000000..e03da26f6b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.wallets.usecase + +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.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for rename user wallet + * + * @property userWalletsListManager user wallets list manager + */ +class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { + + suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either { + val existingNames = userWalletsListManager.userWalletsSync + + if (existingNames.any { it.name == name && it.walletId != userWalletId }) { + return UpdateWalletError.NameAlreadyExists.left() + } + + return either { + userWalletsListManager.update(userWalletId) { it.copy(name = name) } + .doOnSuccess { return it.right() } + .doOnFailure { return UpdateWalletError.DataError.left() } + + return UpdateWalletError.DataError.left() + } + } +} \ 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 8e25c40c1e..469ef7b1fc 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 @@ -7,27 +7,21 @@ import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet /** * Use case for saving user wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { SaveWalletError.DataError }, - ) - userWalletsListManager.save(userWallet, canOverride) .doOnSuccess { return Unit.right() } .doOnFailure { @@ -35,7 +29,7 @@ class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( it.messageResId, ) - else -> SaveWalletError.DataError + else -> SaveWalletError.DataError(it.messageResId) }.left() } 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 84303de548..da446a2a40 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 @@ -5,8 +5,7 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.SelectWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -14,22 +13,18 @@ import com.tangem.domain.wallets.models.UserWalletId /** * Use case for selecting wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager + * @property reduxStateHolder redux state holder * [REDACTED_AUTHOR] */ class SelectWalletUseCase( - private val walletsStateHolder: WalletsStateHolder, + private val userWalletsListManager: UserWalletsListManager, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { SelectWalletError.DataError }, - ) - return when (val result = userWalletsListManager.select(userWalletId)) { is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) is CompletionResult.Success -> { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt index 9b6bdf4997..4d46212807 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt @@ -5,23 +5,23 @@ import arrow.core.raise.either import arrow.core.raise.ensureNotNull import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UnlockWalletsError /** * Unlock wallets use case * - * @property walletsStateHolder wallets state holder + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { +class UnlockWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either = either { val userWalletsListManager = ensureNotNull( - value = walletsStateHolder.userWalletsListManager?.asLockable(), + value = userWalletsListManager.asLockable(), raise = { UnlockWalletsError.DataError( cause = IllegalStateException("The lockable user wallets list manager could not be found"), 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 3a97779a39..b060418b2e 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 @@ -6,8 +6,7 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -15,22 +14,17 @@ import com.tangem.domain.wallets.models.UserWalletId /** * Use case for updating user wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class UpdateWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, ): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { UpdateWalletError.DataError }, - ) - userWalletsListManager.update(userWalletId, update) .doOnSuccess { return it.right() } .doOnFailure { return UpdateWalletError.DataError.left() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt deleted file mode 100644 index 149b12030d..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ValidateWalletAddressUseCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import arrow.core.Either -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.repository.WalletAddressServiceRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -/** - * Use case for validating wallet address. - */ -class ValidateWalletAddressUseCase( - private val walletAddressServiceRepository: WalletAddressServiceRepository, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - network: Network, - address: String, - ): Either = withContext(dispatchers.io) { - Either.catch { - walletAddressServiceRepository.validateAddress(userWalletId, network, address) - } - } -} \ No newline at end of file diff --git a/features/details/api/.gitignore b/features/details/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/details/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/details/api/build.gradle.kts b/features/details/api/build.gradle.kts new file mode 100644 index 0000000000..5010f64d85 --- /dev/null +++ b/features/details/api/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.details.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Project - Core */ + implementation(projects.core.decompose) + + /* AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt new file mode 100644 index 0000000000..d3ac9fe6f7 --- /dev/null +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt @@ -0,0 +1,13 @@ +package com.tangem.features.details + +import androidx.fragment.app.Fragment + +interface DetailsEntryPoint { + + fun entryFragment(): Fragment + + companion object { + + const val USER_WALLET_ID_KEY = "user_wallet_id" + } +} \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsFeatureToggles.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsFeatureToggles.kt new file mode 100644 index 0000000000..15ba44576e --- /dev/null +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.details + +interface DetailsFeatureToggles { + + val isRedesignEnabled: Boolean +} \ No newline at end of file diff --git a/features/details/impl/.gitignore b/features/details/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/details/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts new file mode 100644 index 0000000000..5060249621 --- /dev/null +++ b/features/details/impl/build.gradle.kts @@ -0,0 +1,50 @@ +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.details.impl" +} + +dependencies { + + /* Project - API */ + implementation(projects.features.details.api) + implementation(projects.features.tester.api) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.analytics.models) + + /* Project - Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.legacy) + + /* AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /* Compose */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* Other */ + implementation(deps.kotlin.immutable.collections) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DefaultDetailsFeatureToggles.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DefaultDetailsFeatureToggles.kt new file mode 100644 index 0000000000..dc07bef55f --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DefaultDetailsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.details + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultDetailsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : DetailsFeatureToggles { + + override val isRedesignEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("DETAILS_REDESIGN_ENABLED") +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt new file mode 100644 index 0000000000..28db59fc3d --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt @@ -0,0 +1,76 @@ +package com.tangem.features.details + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.Fragment +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.di.RootAppComponentContext +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.message.EventMessageHandler +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.component.preview.PreviewDetailsComponent +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +// TODO: Remove after [REDACTED_JIRA] +@AndroidEntryPoint +internal class DetailsFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + // @Inject + // internal lateinit var componentFactory: DetailsComponent.Factory + + @Inject + internal lateinit var detailsRouter: DetailsRouter + + @Inject + @RootAppComponentContext + internal lateinit var rootContext: AppComponentContext + + private val component: DetailsComponent by lazy { initComponent() } + + private val messageHandler = EventMessageHandler() + + @Composable + override fun ScreenContent(modifier: Modifier) { + component.View(modifier = modifier) + + EventMessageEffect( + messageHandler = messageHandler, + snackbarHostState = component.snackbarHostState, + ) + } + + private fun initComponent(): DetailsComponent { + // TODO: Uncomment in [REDACTED_JIRA] + // val selectedUserWalletId = arguments?.getString(DetailsEntryPoint.USER_WALLET_ID_KEY) + // ?.let(::UserWalletId) + // + // + // requireNotNull(selectedUserWalletId) { "UserWalletId must be provided" } + // + // val context = rootContext.childByContext( + // componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + // messageHandler = messageHandler, + // router = detailsRouter, + // ) + // + // return componentFactory.create( + // context = context, + // params = DetailsComponent.Params( + // selectedUserWalletId = selectedUserWalletId, + // ), + // ) + + return PreviewDetailsComponent() + } + + companion object : DetailsEntryPoint { + + override fun entryFragment(): Fragment = DetailsFragment() + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt new file mode 100644 index 0000000000..fbd703a49a --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -0,0 +1,55 @@ +package com.tangem.features.details + +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.features.details.routing.DetailsRoute +import com.tangem.features.tester.api.TesterRouter +import javax.inject.Inject + +// TODO: Remove after [REDACTED_JIRA] +internal class DetailsRouter @Inject constructor( + private val reduxNavController: ReduxNavController, + private val reduxStateHolder: ReduxStateHolder, + private val testerRouter: TesterRouter, +) : Router { + + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + if (route is DetailsRoute) { + when (route) { + is DetailsRoute.Screen -> { + reduxNavController.navigate(NavigationAction.NavigateTo(route.screen, bundle = route.params)) + } + is DetailsRoute.Feedback -> { + reduxStateHolder.sendFeedbackEmail() + } + is DetailsRoute.TesterMenu -> { + testerRouter.startTesterScreen() + } + is DetailsRoute.Url -> { + reduxNavController.navigate(NavigationAction.OpenUrl(route.url)) + } + } + onComplete(true) + } else { + onComplete(false) + } + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + reduxNavController.popBackStack() + onComplete(true) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + if (route is DetailsRoute.Screen) { + reduxNavController.popBackStack(route.screen) + onComplete(true) + } else { + reduxNavController.getBackStack() + onComplete(false) + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt new file mode 100644 index 0000000000..9731597b9b --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -0,0 +1,25 @@ +package com.tangem.features.details.component + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.wallets.models.UserWalletId + +interface DetailsComponent { + + val snackbarHostState: SnackbarHostState + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun View(modifier: Modifier) + + interface Factory { + + fun create(context: AppComponentContext, params: Params): DetailsComponent + } + + data class Params( + val selectedUserWalletId: UserWalletId, + ) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt new file mode 100644 index 0000000000..0a76b5fcd1 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.details.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext + +interface UserWalletListComponent { + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun View(modifier: Modifier) + + interface Factory { + fun create(context: AppComponentContext): UserWalletListComponent + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt new file mode 100644 index 0000000000..c8a317814b --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.details.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.wallets.models.UserWalletId + +interface WalletConnectComponent { + + suspend fun checkIsAvailable(): Boolean + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun View(modifier: Modifier) + + interface Factory { + fun create(context: AppComponentContext, params: Params): WalletConnectComponent + } + + data class Params( + val userWalletId: UserWalletId, + ) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt new file mode 100644 index 0000000000..9c6aff4521 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.details.component.preview + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.entity.DetailsFooterUM +import com.tangem.features.details.entity.DetailsUM +import com.tangem.features.details.ui.DetailsScreen +import com.tangem.features.details.utils.ItemsBuilder +import com.tangem.features.details.utils.SocialsBuilder +import kotlinx.coroutines.runBlocking + +internal class PreviewDetailsComponent : DetailsComponent { + + override val snackbarHostState: SnackbarHostState = SnackbarHostState() + + private val previewBlocks = runBlocking { + ItemsBuilder( + walletConnectComponent = PreviewWalletConnectComponent(), + userWalletListComponent = PreviewUserWalletListComponent(), + router = PreviewRouter(), + ).buldAll() + } + + private val previewFooter = DetailsFooterUM( + socials = SocialsBuilder(PreviewRouter()).buildAll(), + appVersion = "1.0.0-preview", + ) + + val previewState = DetailsUM( + items = previewBlocks, + footer = previewFooter, + popBack = { /* no-op */ }, + ) + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + override fun View(modifier: Modifier) { + DetailsScreen( + modifier = modifier, + state = previewState, + snackbarHostState = snackbarHostState, + ) + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt new file mode 100644 index 0000000000..49366db923 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt @@ -0,0 +1,18 @@ +package com.tangem.features.details.component.preview + +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.decompose.navigation.Router + +internal class PreviewRouter : Router { + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + /* no-op */ + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + /* no-op */ + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + /* no-op */ + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt new file mode 100644 index 0000000000..c7628c2619 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -0,0 +1,64 @@ +package com.tangem.features.details.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.impl.R +import com.tangem.features.details.ui.UserWalletListBlock +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewUserWalletListComponent : UserWalletListComponent { + + private val previewState = UserWalletListUM( + userWallets = persistentListOf( + UserWalletListUM.UserWalletUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = "My Wallet", + information = getInformation(3, "4 496,75 $"), + imageResId = R.drawable.ill_card_wallet_2_211_343, + onClick = {}, + ), + UserWalletListUM.UserWalletUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = "Old wallet", + information = getInformation(3, "4 496,75 $"), + imageResId = R.drawable.ill_card_note_eth_211_343, + onClick = {}, + ), + UserWalletListUM.UserWalletUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = "Multi Card", + information = getInformation(3, "4 496,75 $"), + imageResId = R.drawable.ill_card_note_bnb_211_343, + onClick = {}, + ), + ), + addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), + isWalletSavingInProgress = false, + onAddNewWalletClick = {}, + ) + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + override fun View(modifier: Modifier) { + UserWalletListBlock(state = previewState, modifier = modifier) + } + + private fun getInformation(cardCount: Int, totalBalance: String): TextReference { + val t1 = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + val divider = stringReference(value = " • ") + val t2 = stringReference(totalBalance) + + return TextReference.Combined(wrappedList(t1, divider, t2)) + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt new file mode 100644 index 0000000000..4f94585d78 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.details.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.details.component.WalletConnectComponent +import com.tangem.features.details.ui.WalletConnectBlock + +internal class PreviewWalletConnectComponent : WalletConnectComponent { + + override suspend fun checkIsAvailable(): Boolean = true + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + override fun View(modifier: Modifier) { + WalletConnectBlock(onClick = { /* no-op */ }, modifier = modifier) + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt new file mode 100644 index 0000000000..f9a437ee25 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt @@ -0,0 +1,29 @@ +package com.tangem.features.details.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.details.DefaultDetailsFeatureToggles +import com.tangem.features.details.DetailsEntryPoint +import com.tangem.features.details.DetailsFeatureToggles +import com.tangem.features.details.DetailsFragment +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 FeatureModule { + + @Provides + @Singleton + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles { + return DefaultDetailsFeatureToggles(featureTogglesManager) + } + + @Provides + @Singleton + fun provideEntryPoint(): DetailsEntryPoint { + return DetailsFragment.Companion + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt new file mode 100644 index 0000000000..8b429e0438 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.details.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.details.model.DetailsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(DetailsModel::class) + fun provideDetailsModel(model: DetailsModel): Model +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt new file mode 100644 index 0000000000..267acbe9d0 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt @@ -0,0 +1,17 @@ +package com.tangem.features.details.entity + +import androidx.annotation.DrawableRes +import kotlinx.collections.immutable.ImmutableList + +internal data class DetailsFooterUM( + val appVersion: String, + val socials: ImmutableList, +) { + + data class Social( + val id: String, + @DrawableRes + val iconResId: Int, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt new file mode 100644 index 0000000000..1aa5a52ab5 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt @@ -0,0 +1,41 @@ +package com.tangem.features.details.entity + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class DetailsItemUM { + + abstract val id: String + + data class Basic( + override val id: String, + val items: ImmutableList, + ) : DetailsItemUM() { + + data class Item( + val id: String, + val title: TextReference, + @DrawableRes + val iconRes: Int, + val onClick: () -> Unit, + ) + } + + data class Component( + override val id: String, + val content: Content, + ) : DetailsItemUM() { + + fun interface Content { + + @Composable + @Suppress("TopLevelComposableFunctions", "ComposableFunctionName") + operator fun invoke(modifier: Modifier) + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt new file mode 100644 index 0000000000..15f0fead96 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.details.entity + +import kotlinx.collections.immutable.ImmutableList + +internal data class DetailsUM( + val items: ImmutableList, + val footer: DetailsFooterUM, + val popBack: () -> Unit, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..d6f947b063 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.details.entity + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.collections.immutable.ImmutableList + +internal data class UserWalletListUM( + val userWallets: ImmutableList, + val isWalletSavingInProgress: Boolean, + val addNewWalletText: TextReference, + val onAddNewWalletClick: () -> Unit, +) { + + data class UserWalletUM( + val id: UserWalletId, + val name: String, + val information: TextReference, + @DrawableRes + val imageResId: Int, + val onClick: () -> Unit, + ) +} \ 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 new file mode 100644 index 0000000000..918c27eb97 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -0,0 +1,10 @@ +package com.tangem.features.details.model + +import com.tangem.core.decompose.model.Model +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +// TODO: Will be implemented later +internal class DetailsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt new file mode 100644 index 0000000000..878987aca5 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt @@ -0,0 +1,20 @@ +package com.tangem.features.details.routing + +import android.os.Bundle +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.navigation.AppScreen + +// TODO: Remove after [REDACTED_JIRA] +internal sealed class DetailsRoute : Route { + + data class Screen( + val screen: AppScreen, + val params: Bundle? = null, + ) : DetailsRoute() + + data class Url(val url: String) : DetailsRoute() + + data object Feedback : DetailsRoute() + + data object TesterMenu : DetailsRoute() +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt new file mode 100644 index 0000000000..e11a41c41e --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt @@ -0,0 +1,36 @@ +package com.tangem.features.details.ui + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun BlockCard( + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit = {}, + content: @Composable ColumnScope.() -> Unit = {}, +) { + Card( + modifier = modifier, + onClick = onClick, + shape = TangemTheme.shapes.roundedCornersXMedium, + colors = BlockColors, + enabled = enabled, + content = content, + ) +} + +private val BlockColors: CardColors + @Composable + @ReadOnlyComposable + get() = CardColors( + containerColor = TangemTheme.colors.background.primary, + contentColor = TangemTheme.colors.text.primary1, + disabledContainerColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt new file mode 100644 index 0000000000..80d99b3b20 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt @@ -0,0 +1,45 @@ +package com.tangem.features.details.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +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.TextOverflow +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.details.entity.DetailsItemUM + +@Composable +internal fun BlockItem(model: DetailsItemUM.Basic.Item, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + onClick = model.onClick, + ) { + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = model.iconRes), + tint = TangemTheme.colors.icon.secondary, + contentDescription = null, + ) + + Text( + text = model.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt new file mode 100644 index 0000000000..0b03305816 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -0,0 +1,228 @@ +package com.tangem.features.details.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.details.component.preview.PreviewDetailsComponent +import com.tangem.features.details.entity.DetailsFooterUM +import com.tangem.features.details.entity.DetailsItemUM +import com.tangem.features.details.entity.DetailsUM +import com.tangem.features.details.impl.R + +private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) { + val backgroundColor = TangemTheme.colors.background.secondary + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + SystemBarsEffect { + setSystemBarsColor(backgroundColor) + } + + BackHandler(onBack = state.popBack) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = backgroundColor, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + hostState = snackbarHostState, + ) + }, + topBar = { TopBar(state, scrollBehavior) }, + ) { paddingValues -> + Content( + modifier = Modifier.padding(paddingValues), + state = state, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier) { + MediumTopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = TopAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + scrolledContainerColor = TangemTheme.colors.background.secondary, + navigationIconContentColor = TangemTheme.colors.icon.primary1, + titleContentColor = TangemTheme.colors.text.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ), + title = { + val collapsedStyle = TangemTheme.typography.subtitle1 + val expandedStyle = TangemTheme.typography.h1 + val style by remember(scrollBehavior.state.collapsedFraction) { + derivedStateOf { + if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) { + collapsedStyle + } else { + expandedStyle + } + } + } + + Text( + text = stringResource(id = R.string.details_title), + style = style, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = state.popBack, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + ) + } + }, + ) +} + +@Composable +private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + items( + items = state.items, + key = DetailsItemUM::id, + ) { block -> + Block( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + model = block, + ) + } + + item(key = "footer") { + Footer( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + model = state.footer, + ) + } + } +} + +@Composable +private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + ), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + ) { + when (model) { + is DetailsItemUM.Basic -> { + model.items.forEach { item -> + key(item.id) { + BlockItem( + modifier = Modifier.fillMaxWidth(), + model = item, + ) + } + } + } + is DetailsItemUM.Component -> { + model.content( + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + val socialsScrollState = rememberScrollState() + + Row( + modifier = Modifier + .fillMaxWidth() + .scrollable(socialsScrollState, orientation = Orientation.Horizontal), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + model.socials.forEach { social -> + key(social.id) { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = social.onClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = social.iconResId), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } + } + } + + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + text = model.appVersion, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_DetailsScreen() { + TangemThemePreview { + PreviewDetailsComponent().View(modifier = Modifier.fillMaxSize()) + } +} +// endregion Preview \ 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 new file mode 100644 index 0000000000..254c434047 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -0,0 +1,121 @@ +package com.tangem.features.details.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.impl.R + +@Composable +internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + ) { + state.userWallets.forEach { model -> + key(model.id) { + UserWalletItem( + modifier = Modifier.fillMaxWidth(), + model = model, + ) + } + } + AddWalletButton( + text = state.addNewWalletText, + isInProgress = state.isWalletSavingInProgress, + onClick = state.onAddNewWalletClick, + ) + } +} + +@Composable +private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + onClick = model.onClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size68) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Image( + modifier = Modifier + .width(TangemTheme.dimens.size24) + .height(TangemTheme.dimens.size36), + painter = painterResource(id = model.imageResId), + contentScale = ContentScale.FillBounds, + contentDescription = null, + ) + + Column( + modifier = Modifier.heightIn(min = TangemTheme.dimens.size40), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Text( + text = model.name, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = model.information.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +@Composable +private fun AddWalletButton( + text: TextReference, + isInProgress: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BlockCard( + modifier = modifier, + onClick = onClick, + enabled = !isInProgress, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_plus_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + + Text( + text = text.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.accent, + ) + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt new file mode 100644 index 0000000000..11a28b3a75 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt @@ -0,0 +1,51 @@ +package com.tangem.features.details.ui + +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.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.details.impl.R + +@Composable +internal fun WalletConnectBlock(onClick: () -> Unit, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + onClick = onClick, + ) { + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_wallet_connect_24), + tint = TangemColorPalette.Azure, + contentDescription = null, + ) + + Column( + modifier = Modifier.heightIn(min = TangemTheme.dimens.size48), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + text = stringResource(id = R.string.wallet_connect_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResource(id = R.string.wallet_connect_subtitle), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt new file mode 100644 index 0000000000..aa18331772 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -0,0 +1,105 @@ +package com.tangem.features.details.utils + +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.AppScreen +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.component.WalletConnectComponent +import com.tangem.features.details.entity.DetailsItemUM +import com.tangem.features.details.impl.BuildConfig +import com.tangem.features.details.impl.R +import com.tangem.features.details.routing.DetailsRoute +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class ItemsBuilder( + private val walletConnectComponent: WalletConnectComponent, + private val userWalletListComponent: UserWalletListComponent, + private val router: Router, +) { + + suspend fun buldAll(): ImmutableList = buildList { + buildWalletConnectBlock()?.let(::add) + buildUserWalletListBlock().let(::add) + buildShopBlock().let(::add) + buildSettingsBlock().let(::add) + buildSupportBlock().let(::add) + }.toImmutableList() + + private suspend fun buildWalletConnectBlock(): DetailsItemUM? { + return if (walletConnectComponent.checkIsAvailable()) { + DetailsItemUM.Component( + id = "wallet_connect", + content = { + walletConnectComponent.View(modifier = it) + }, + ) + } else { + null + } + } + + private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.Component( + id = "user_wallet_list", + content = { + userWalletListComponent.View(modifier = it) + }, + ) + + private fun buildShopBlock(): DetailsItemUM = DetailsItemUM.Basic( + id = "shop", + items = persistentListOf( + DetailsItemUM.Basic.Item( + id = "buy_tangem_wallet", + title = stringReference("Buy Tangem Wallet"), // TODO: Move to resources in [REDACTED_TASK_KEY] + iconRes = R.drawable.ic_tangem_24, + onClick = { router.push(DetailsRoute.Url(BUY_TANGEM_URL)) }, + ), + ), + ) + + private fun buildSettingsBlock(): DetailsItemUM = DetailsItemUM.Basic( + id = "settings", + items = buildList { + DetailsItemUM.Basic.Item( + id = "app_settings", + title = resourceReference(R.string.app_settings_title), + iconRes = R.drawable.ic_settings_24, + onClick = { router.push(DetailsRoute.Screen(AppScreen.AppSettings)) }, + ).let(::add) + + if (BuildConfig.TESTER_MENU_ENABLED) { + DetailsItemUM.Basic.Item( + id = "tester_menu", + title = stringReference(value = "Tester menu"), + iconRes = R.drawable.ic_alert_24, + onClick = { router.push(DetailsRoute.TesterMenu) }, + ).let(::add) + } + }.toImmutableList(), + ) + + private fun buildSupportBlock(): DetailsItemUM = DetailsItemUM.Basic( + id = "support", + items = persistentListOf( + DetailsItemUM.Basic.Item( + id = "send_feedback", + title = stringReference("Send feedback"), // TODO: Move to resources in [REDACTED_TASK_KEY] + iconRes = R.drawable.ic_comment_24, + onClick = { router.push(DetailsRoute.Feedback) }, + ), + DetailsItemUM.Basic.Item( + id = "disclaimer", + title = resourceReference(R.string.disclaimer_title), + iconRes = R.drawable.ic_text_24, + onClick = { router.push(DetailsRoute.Screen(AppScreen.Disclaimer)) }, + ), + ), + ) + + private companion object { + const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app" + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt new file mode 100644 index 0000000000..6896f1e7d0 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt @@ -0,0 +1,88 @@ +package com.tangem.features.details.utils + +import androidx.compose.ui.text.intl.Locale +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.details.entity.DetailsFooterUM +import com.tangem.features.details.impl.R +import com.tangem.features.details.routing.DetailsRoute +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class SocialsBuilder( + private val router: Router, +) { + + fun buildAll(): ImmutableList = Social.all.map { social -> + DetailsFooterUM.Social( + id = social.name, + iconResId = social.iconResId, + onClick = { openUrl(social) }, + ) + }.toImmutableList() + + private fun openUrl(social: Social) { + val locale = Locale.current.region + + val url = if (locale == RUSSIA_LOCALE && social.urlRu != null) { + social.urlRu + } else { + social.url + } + + router.push(DetailsRoute.Url(url)) + } + + private enum class Social( + val iconResId: Int, + val url: String, + val urlRu: String? = null, + ) { + X( + iconResId = R.drawable.ic_twitter_24, + url = "https://x.com/tangem", + ), + TELEGRAM( + iconResId = R.drawable.ic_telegram_24, + url = "https://t.me/tangem_chat", + urlRu = "https://t.me/tangem_chat_ru", + ), + DISCORD( + iconResId = R.drawable.ic_discord_24, + url = "https://discord.gg/tangem", + ), + REDDIT( + iconResId = R.drawable.ic_reddit_24, + url = "https://www.reddit.com/r/Tangem/", + ), + INSTAGRAM( + iconResId = R.drawable.ic_instagram_24, + url = "https://www.instagram.com/tangemwallet", + ), + GIT_HUB( + iconResId = R.drawable.ic_github_24, + url = "https://github.com/tangem", + ), + FACEBOOK( + iconResId = R.drawable.ic_facebook_24, + url = "https://www.facebook.com/tangemwallet", + ), + LINKEDIN( + iconResId = R.drawable.ic_linkedin_24, + url = "https://www.linkedin.com/company/tangem", + ), + YOUTUBE( + iconResId = R.drawable.ic_youtube_24, + url = "https://youtube.com/@tangem_official", + ), + ; + + companion object { + + val all = values() + } + } + + private companion object { + const val RUSSIA_LOCALE = "ru" + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/res/drawable/ill_card_note_bnb_211_343.png b/features/details/impl/src/main/res/drawable/ill_card_note_bnb_211_343.png new file mode 100644 index 0000000000..033efd2699 Binary files /dev/null and b/features/details/impl/src/main/res/drawable/ill_card_note_bnb_211_343.png differ diff --git a/features/details/impl/src/main/res/drawable/ill_card_note_eth_211_343.png b/features/details/impl/src/main/res/drawable/ill_card_note_eth_211_343.png new file mode 100644 index 0000000000..b338ee1eb5 Binary files /dev/null and b/features/details/impl/src/main/res/drawable/ill_card_note_eth_211_343.png differ diff --git a/features/details/impl/src/main/res/drawable/ill_card_wallet_2_211_343.png b/features/details/impl/src/main/res/drawable/ill_card_wallet_2_211_343.png new file mode 100644 index 0000000000..9625b941a1 Binary files /dev/null and b/features/details/impl/src/main/res/drawable/ill_card_wallet_2_211_343.png differ diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 8fb983d6d7..88d462b52c 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt index d335aaca71..d5691e9017 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -24,6 +25,7 @@ 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.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.AlertState import com.tangem.managetokens.presentation.common.state.ChooseWalletState @@ -287,17 +289,10 @@ private fun TokenTextFieldTitle(state: TextFieldState?, title: String) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseDerivationScreen_Light() { - TangemTheme(isDark = false) { - AddCustomTokenScreen(state = AddCustomTokenPreviewData.state) - } -} - -@Preview -@Composable -private fun Preview_ChooseDerivationScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseDerivationScreen() { + TangemThemePreview { AddCustomTokenScreen(state = AddCustomTokenPreviewData.state) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt index 3422e1cf8e..a7805cbb96 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,6 +14,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock @@ -98,17 +100,10 @@ private fun DerivationsList(state: ChooseDerivationState) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseDerivationScreen_Light() { - TangemTheme(isDark = false) { - ChooseDerivationScreen(state = ChooseDerivationPreviewData.state) - } -} - -@Preview -@Composable -private fun Preview_ChooseDerivationScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseDerivationScreen() { + TangemThemePreview { ChooseDerivationScreen(state = ChooseDerivationPreviewData.state) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt index 4ee903652e..6777fc4fbc 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,6 +14,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.ui.components.NetworkItem @@ -78,17 +80,10 @@ internal fun ChooseNetworkCustomScreen(state: ChooseNetworkState, modifier: Modi } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseNetworkScreen_Light() { - TangemTheme(isDark = false) { - ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state) - } -} - -@Preview -@Composable -private fun Preview_ChooseNetworkScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseNetworkScreen() { + TangemThemePreview { ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt index 6d79799bc2..82ba359079 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt @@ -43,10 +43,10 @@ internal sealed class AlertState { ) } - class CannotHideNetworkWithTokens(tokenName: String, networkName: String) : AlertState() { + class CannotHideNetworkWithTokens(tokenName: String, currencySymbol: String, networkName: String) : AlertState() { override val message: TextReference = resourceReference( id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(tokenName, networkName), + formatArgs = wrappedList(tokenName, currencySymbol, networkName), ) } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt index 878a5f28e1..121faaba26 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.common.ui +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,6 +24,7 @@ import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.ChooseWalletState import com.tangem.managetokens.presentation.common.state.WalletState @@ -137,19 +139,10 @@ private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifi } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseWalletScreen_Light() { - TangemTheme(isDark = false) { - ChooseWalletScreen( - state = ChooseWalletStatePreviewData.state, - ) - } -} - -@Preview -@Composable -private fun Preview_ChooseWalletScreen_Dark() { - TangemTheme(isDark = false) { +private fun Preview_ChooseWalletScreen() { + TangemThemePreview { ChooseWalletScreen( state = ChooseWalletStatePreviewData.state, ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt index 0359b28aa1..553a6bd999 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.common.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.NetworkItemState @@ -122,17 +124,10 @@ internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { - TangemTheme(isDark = false) { - NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) - } -} - -@Preview -@Composable -private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { - TangemTheme(isDark = true) { +private fun Preview_NetworkItem(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemThemePreview { NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt index c136cb1e28..e7ad5cf31b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.common.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -12,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @Composable @@ -54,17 +56,10 @@ fun SimpleSelectionBlock( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SimpleSelectionBlock_Light() { - TangemTheme(isDark = false) { - SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) - } -} - -@Preview -@Composable -private fun Preview_SimpleSelectionBlock_Dark() { - TangemTheme(isDark = true) { +private fun Preview_SimpleSelectionBlock() { + TangemThemePreview { SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt index ead0700da2..2f5d51df8a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.WarningCardTitleOnly import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.ChooseWalletState @@ -187,20 +189,10 @@ private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseNetworkScreen_Light() { - TangemTheme(isDark = false) { - ChooseNetworkScreen( - state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, - walletState = ChooseWalletStatePreviewData.state, - ) - } -} - -@Preview -@Composable -private fun Preview_ChooseNetworkScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseNetworkScreen() { + TangemThemePreview { ChooseNetworkScreen( state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, walletState = ChooseWalletStatePreviewData.state, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt index 12f7a0a191..15197a8e4a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui +import android.content.res.Configuration import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH18 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenBottomSheet import com.tangem.managetokens.presentation.common.state.AlertState @@ -177,23 +179,13 @@ private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ManageTokensScreen_LightTheme( +private fun Preview_ManageTokensScreen( @PreviewParameter(ManageTokensConfigProvider::class) state: ManageTokensState, ) { - TangemTheme(isDark = false) { - ManageTokensScreen(state) {} - } -} - -@Preview -@Composable -private fun Preview_ManageTokensScreen_DarkTheme( - @PreviewParameter(ManageTokensConfigProvider::class) - state: ManageTokensState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ManageTokensScreen(state) {} } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt index 195bb4d60c..32962c70db 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -14,6 +15,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.impl.R @Composable @@ -49,17 +51,10 @@ internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AddCustomTokenButton_Preview_Light() { - TangemTheme(isDark = false) { - AddCustomTokenButton(onButtonClick = { }) - } -} - -@Preview -@Composable -private fun AddCustomTokenButton_Preview_Dark() { - TangemTheme(isDark = true) { +private fun AddCustomTokenButton_Preview() { + TangemThemePreview { AddCustomTokenButton(onButtonClick = { }) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt index a18fffaf56..d1f3822c9b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -18,6 +19,7 @@ import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData @@ -127,17 +129,10 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ManageTokensScreen_LightTheme() { - TangemTheme(isDark = false) { - DerivationNotification(DerivationNotificationStatePreviewData.state.config) - } -} - -@Preview -@Composable -private fun Preview_ManageTokensScreen_DarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_ManageTokensScreen() { + TangemThemePreview { DerivationNotification(DerivationNotificationStatePreviewData.state.config) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt index 998dbebc72..1a18c5ee93 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -105,7 +106,7 @@ private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Fl @Preview(widthDp = 150, heightDp = 150, showBackground = true) @Composable private fun Chart_Positive_Preview() { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { PriceChangesChart( persistentListOf(1f, 2f, 4f, 1f, 5f), ) @@ -115,7 +116,7 @@ private fun Chart_Positive_Preview() { @Preview(widthDp = 150, heightDp = 150, showBackground = true) @Composable private fun Chart_Negative_Preview() { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { PriceChangesChart( persistentListOf(10f, 2f, 4f, 1f, 5f), ) @@ -125,7 +126,7 @@ private fun Chart_Negative_Preview() { @Preview(widthDp = 150, heightDp = 150, showBackground = true) @Composable private fun Chart_Neutral_Preview() { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { PriceChangesChart( persistentListOf(5f, 2f, 4f, 1f, 5f), ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt index 6040c96260..8faf911ebb 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -20,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.managetokens.state.SearchBarState @@ -107,20 +109,13 @@ private fun searchbarTextFieldColors(): TextFieldColors { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokensSearchBar_Light( +private fun Preview_TokensSearchBar( @PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState, ) { - TangemTheme(isDark = false) { - TokensSearchBar(state) - } -} - -@Preview -@Composable -private fun Preview_TokensSearchBar_Dark(@PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState) { - TangemTheme(isDark = true) { + TangemThemePreview { TokensSearchBar(state) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt index f3f76effd3..3829ec91db 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -18,6 +19,7 @@ import com.tangem.core.ui.components.buttons.PrimarySmallButton 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.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType @@ -66,17 +68,10 @@ internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: M } @Preview(backgroundColor = 0xffffff, showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TokenButton_Light_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { - TangemTheme(isDark = false) { - TokenButton(type = type, {}) - } -} - -@Preview(showBackground = true) -@Composable -private fun TokenButton_Dark_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { - TangemTheme(isDark = true) { +private fun TokenButton_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { + TangemThemePreview { TokenButton(type = type, {}) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt index d92f0a8b81..50cf504643 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Surface @@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.managetokens.presentation.managetokens.state.QuotesState import com.tangem.managetokens.presentation.managetokens.state.TokenItemState @@ -182,17 +184,10 @@ private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () - // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemTheme(isDark = false) { - TokenRowItem(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemTheme(isDark = true) { +private fun Preview_Tokens(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { + TangemThemePreview { TokenRowItem(state) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt index 11467bfa98..caa5ca105b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt @@ -391,6 +391,7 @@ internal class ManageTokensViewModel @Inject constructor( event = Event.ShowAlert( AlertState.CannotHideNetworkWithTokens( tokenName = cryptoCurrency.name, + currencySymbol = cryptoCurrency.symbol, networkName = cryptoCurrency.network.name, ), ), diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index abc2bfff70..de2ffddcbc 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,6 +22,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState @@ -204,11 +206,12 @@ private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: D } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SuggestionsBlockPreview_Light( +private fun SuggestionsBlockPreview( @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, ) { - TangemTheme(isDark = false) { + TangemThemePreview { SuggestionsBlock( suggestionsList = suggestions, onClick = {}, @@ -217,24 +220,12 @@ private fun SuggestionsBlockPreview_Light( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SuggestionsBlockPreview_Dark( +private fun ImportSeedPhraseScreenPreview( @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, ) { - TangemTheme(isDark = true) { - SuggestionsBlock( - suggestionsList = suggestions, - onClick = {}, - ) - } -} - -@Preview -@Composable -private fun ImportSeedPhraseScreenPreview_Light( - @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, -) { - TangemTheme(isDark = false) { + TangemThemePreview { Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { ImportSeedPhraseScreen( ImportSeedPhraseState( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt index ee04bfe228..9228432a75 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent @@ -83,7 +84,7 @@ fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetConte @Preview @Composable private fun PassphraseInfoBottomSheetContentPreview() { - TangemTheme { + TangemThemePreview { PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { }) } } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt index 609e2df784..2d767dc78a 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.* import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText @@ -105,7 +107,7 @@ private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modif it.count, ), modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing16) + .padding(vertical = TangemTheme.dimens.spacing10) .fillMaxWidth(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, @@ -163,10 +165,11 @@ private inline fun VerticalGrid( } } -@Preview +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun YourSeedPhraseScreenPreview_Light() { - TangemTheme(isDark = false) { + TangemThemePreview { YourSeedPhraseScreen( state = YourSeedPhraseState( segmentSeedState = SegmentSeedState( diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index f232473da8..72d725c460 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -20,9 +20,9 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.mlkit.vision.common.InputImage +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningContent @@ -39,7 +39,7 @@ import kotlin.properties.Delegates internal class QrScanningFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var router: QrScanningRouter diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 5125499148..5ca72579ad 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 2496fde6e8..2d44c137ff 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -2,12 +2,13 @@ package com.tangem.feature.referral.data import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody import com.tangem.datasource.demo.DemoModeDatasource import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -15,7 +16,6 @@ import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData -import com.tangem.lib.auth.AuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 2214c244dd..23164dbe82 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -1,12 +1,12 @@ package com.tangem.feature.referral.di +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.demo.DemoModeDatasource import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.data.ReferralRepositoryImpl import com.tangem.feature.referral.domain.ReferralRepository -import com.tangem.lib.auth.AuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index e7a122fb45..2ccd4b74fc 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen import com.tangem.feature.referral.viewmodels.ReferralViewModel @@ -20,7 +20,7 @@ import javax.inject.Inject class ReferralFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel by viewModels() diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index bc175c5bff..e44e192d0d 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -1,5 +1,6 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -16,6 +17,7 @@ import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.referral.presentation.R @@ -58,17 +60,10 @@ private fun AgreementHtmlView(url: String) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementBottomSheet_InLightTheme() { - TangemTheme(isDark = false) { - AgreementBottomSheetContent(url = "https://tangem.com/en/") - } -} - -@Preview -@Composable -private fun Preview_AgreementBottomSheet_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AgreementBottomSheet() { + TangemThemePreview { AgreementBottomSheetContent(url = "https://tangem.com/en/") } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt index d29a6c996b..e005f91c7b 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -1,5 +1,6 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -15,6 +16,7 @@ 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.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.referral.presentation.R @@ -53,19 +55,10 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementText_InLightTheme() { - TangemTheme(isDark = false) { - Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { - AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = {}) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_AgreementText_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AgreementText() { + TangemThemePreview { Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = {}) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt index a85c1a0f07..5f11c074e1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview @Suppress("LongParameterList") @Composable @@ -56,8 +57,8 @@ internal fun AwardText( @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_AwardItem_Light() { - TangemTheme { +private fun Preview_AwardItem() { + TangemThemePreview { AwardText( startText = "startText", startTextColor = TangemTheme.colors.text.tertiary, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt index 78a8239dc0..077fb745d0 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -1,14 +1,17 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration 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.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryEndIconButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R @Composable @@ -18,29 +21,23 @@ internal fun NonParticipateBottomBlock(onAgreementClick: () -> Unit, onParticipa firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = onAgreementClick, ) - PrimaryEndIconButton( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + + PrimaryButtonIconEnd( text = stringResource(id = R.string.referral_button_participate), iconResId = R.drawable.ic_tangem_24, onClick = onParticipateClick, + modifier = Modifier + .fillMaxWidth() + .padding(all = TangemTheme.dimens.spacing16), ) } } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NonParticipateBottomBlock_InLightTheme() { - TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { - NonParticipateBottomBlock(onAgreementClick = {}, onParticipateClick = {}) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_NonParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_NonParticipateBottomBlock() { + TangemThemePreview { Column(Modifier.background(TangemTheme.colors.background.primary)) { NonParticipateBottomBlock(onAgreementClick = {}, onParticipateClick = {}) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 3417b6931f..cd403043df 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -2,19 +2,14 @@ package com.tangem.feature.referral.ui import android.content.Context import android.content.Intent +import android.content.res.Configuration import androidx.compose.animation.* 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.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -30,11 +25,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.presentation.R +import kotlinx.coroutines.launch @Suppress("LongParameterList") @Composable @@ -43,8 +40,8 @@ internal fun ParticipateBottomBlock( code: String, shareLink: String, expectedAwards: ExpectedAwards?, + snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, - onShowCopySnackbar: () -> Unit, onCopyClick: () -> Unit, onShareClick: () -> Unit, ) { @@ -61,7 +58,7 @@ internal fun ParticipateBottomBlock( AdditionalButtons( code = code, shareLink = shareLink, - onShowCopySnackbar = onShowCopySnackbar, + snackbarHostState = snackbarHostState, onCopyClick = onCopyClick, onShareClick = onShareClick, ) @@ -110,9 +107,9 @@ private fun Awards(expectedAwards: ExpectedAwards) { val elementsCountToShowInLessMode = 3 val isExpanded = remember { mutableStateOf(false) } - Divider( - color = TangemTheme.colors.stroke.primary, + HorizontalDivider( thickness = TangemTheme.dimens.size0_5, + color = TangemTheme.colors.stroke.primary, ) AwardText( startText = if (expectedAwards.expectedAwards.isNotEmpty()) { @@ -249,7 +246,7 @@ private fun PersonalCodeCard(code: String) { .fillMaxWidth() .padding(vertical = TangemTheme.dimens.spacing12), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( text = stringResource(id = R.string.referral_promo_code_title), @@ -270,32 +267,40 @@ private fun PersonalCodeCard(code: String) { private fun AdditionalButtons( code: String, shareLink: String, - onShowCopySnackbar: () -> Unit, + snackbarHostState: SnackbarHostState, onCopyClick: () -> Unit, onShareClick: () -> Unit, ) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + val resources = LocalContext.current.resources + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { - PrimaryStartIconButton( - modifier = Modifier.weight(1f), + PrimaryButtonIconStart( text = stringResource(id = R.string.common_copy), iconResId = R.drawable.ic_copy_24, onClick = { onCopyClick.invoke() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clipboardManager.setText(AnnotatedString(code)) - onShowCopySnackbar() + + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getString(R.string.referral_promo_code_copied), + duration = SnackbarDuration.Short, + ) + } }, + modifier = Modifier.weight(1f), ) val context = LocalContext.current - PrimaryStartIconButton( - modifier = Modifier.weight(1f), + PrimaryButtonIconStart( text = stringResource(id = R.string.common_share), iconResId = R.drawable.ic_share_24, onClick = { @@ -303,6 +308,7 @@ private fun AdditionalButtons( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) context.shareText(context.getString(R.string.referral_share_link, shareLink)) }, + modifier = Modifier.weight(1f), ) } } @@ -318,11 +324,12 @@ private fun Context.shareText(text: String) { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ParticipateBottomBlockPreview_Light( +private fun ParticipateBottomBlockPreview( @PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData, ) { - TangemTheme(isDark = false) { + TangemThemePreview { Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = data.purchasedWalletCount, @@ -330,7 +337,7 @@ private fun ParticipateBottomBlockPreview_Light( shareLink = data.shareLink, expectedAwards = data.expectedAwards, onAgreementClick = data.onAgreementClick, - onShowCopySnackbar = data.onShowCopySnackbar, + snackbarHostState = SnackbarHostState(), onCopyClick = data.onCopyClick, onShareClick = data.onShareClick, ) @@ -339,42 +346,10 @@ private fun ParticipateBottomBlockPreview_Light( } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ParticipateBottomBlockPreview_Dark( - @PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData, -) { - TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.secondary)) { - ParticipateBottomBlock( - purchasedWalletCount = state.purchasedWalletCount, - code = state.code, - shareLink = state.shareLink, - expectedAwards = state.expectedAwards, - onAgreementClick = state.onAgreementClick, - onShowCopySnackbar = state.onShowCopySnackbar, - onCopyClick = state.onCopyClick, - onShareClick = state.onShareClick, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun LessMoreButton_Light() { - TangemTheme(isDark = false) { - LessMoreButton( - isExpanded = remember { - mutableStateOf(false) - }, - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun LessMoreButton_Dark() { - TangemTheme(isDark = true) { +private fun LessMoreButtonPreview() { + TangemThemePreview { LessMoreButton( isExpanded = remember { mutableStateOf(false) @@ -420,7 +395,6 @@ private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterPro purchasedWalletCount = 0, expectedAwards = null, ), - ), ) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 74f9653422..39b216fb16 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -1,5 +1,7 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration +import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -10,9 +12,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -21,13 +21,15 @@ 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 androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException @@ -48,6 +50,8 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier var isBottomSheetVisible by remember { mutableStateOf(value = false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val snackbarHostState = remember(::SnackbarHostState) + Scaffold( modifier = modifier, topBar = { @@ -56,10 +60,26 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier onBackClick = stateHolder.headerState.onBackClicked, ) }, + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(bottom = TangemTheme.dimens.spacing108), + ) { + // TODO: use StateEvent + if (stateHolder.errorSnackbar != null) { + TangemSnackbar(data = it, actionOnNewLine = true) + } else { + CopiedTextSnackbar(it) + } + } + }, containerColor = TangemTheme.colors.background.secondary, ) { ReferralContent( stateHolder = stateHolder, + snackbarHostState = snackbarHostState, onAgreementClick = { stateHolder.analytics.onAgreementClicked.invoke() isBottomSheetVisible = true @@ -68,6 +88,26 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier ) } + val errorSnackbar = stateHolder.errorSnackbar + val coroutineScope = rememberCoroutineScope() + val resources = LocalContext.current.resources + + SideEffect { + if (errorSnackbar != null) { + coroutineScope.launch { + val result = snackbarHostState.showSnackbar( + message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable), + actionLabel = resources.getString(R.string.warning_button_ok), + duration = SnackbarDuration.Indefinite, + ) + + if (result == SnackbarResult.ActionPerformed) { + errorSnackbar.onOkClicked() + } + } + } + } + ReferralBottomSheet( sheetState = sheetState, isVisible = isBottomSheetVisible, @@ -79,11 +119,10 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier @Composable private fun ReferralContent( stateHolder: ReferralStateHolder, + snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, modifier: Modifier = Modifier, ) { - val isCopyButtonPressed = remember { mutableStateOf(value = false) } - Box(modifier = modifier) { LazyColumn( modifier = Modifier.fillMaxSize(), @@ -93,14 +132,11 @@ private fun ReferralContent( item { ReferralInfo( stateHolder = stateHolder, + snackbarHostState = snackbarHostState, onAgreementClick = onAgreementClick, - onShowCopySnackbar = { isCopyButtonPressed.value = true }, ) } } - - ErrorSnackbarHost(errorSnackbar = stateHolder.errorSnackbar) - CopySnackbarHost(isCopyButtonPressed = isCopyButtonPressed) } } @@ -131,8 +167,8 @@ private fun Header() { @Composable private fun ReferralInfo( stateHolder: ReferralStateHolder, + snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, - onShowCopySnackbar: () -> Unit, ) { when (val state = stateHolder.referralInfoState) { is ReferralInfoState.ParticipantContent -> { @@ -142,8 +178,8 @@ private fun ReferralInfo( code = state.code, shareLink = state.shareLink, expectedAwards = state.expectedAwards, + snackbarHostState = snackbarHostState, onAgreementClick = onAgreementClick, - onShowCopySnackbar = onShowCopySnackbar, onCopyClick = stateHolder.analytics.onCopyClicked, onShareClick = stateHolder.analytics.onShareClicked, ) @@ -333,122 +369,19 @@ private fun ShimmerInfo() { } } -// TODO() Replace component with component from ds -@Composable -private fun BoxScope.ErrorSnackbarHost(errorSnackbar: ErrorSnackbar?) { - if (errorSnackbar != null) { - val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } - val coroutineScope = rememberCoroutineScope() - - SnackbarHost( - hostState = snackbarHostState, - modifier = Modifier.align(Alignment.BottomCenter), - snackbar = { - Snackbar( - snackbarData = it, - modifier = Modifier.fillMaxWidth(), - actionOnNewLine = true, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius8), - containerColor = TangemTheme.colors.button.primary, - contentColor = TangemTheme.colors.text.primary2, - actionColor = TangemTheme.colors.text.primary2, - ) - }, - ) - - val actionLabel = stringResource(id = R.string.warning_button_ok) - val message = getMessageForErrorSnackbar(errorSnackbar) - SideEffect { - coroutineScope.launch { - val result = snackbarHostState.showSnackbar( - message = message, - actionLabel = actionLabel, - duration = SnackbarDuration.Indefinite, - ) - if (result == SnackbarResult.ActionPerformed) { - errorSnackbar.onOkClicked() - } - } - } - } -} - -// TODO() Replace component with component from ds -@Composable -private fun BoxScope.CopySnackbarHost(isCopyButtonPressed: MutableState) { - if (isCopyButtonPressed.value) { - val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } - val coroutineScope = rememberCoroutineScope() - - var snackbarSize by remember { mutableIntStateOf(value = 0) } - val width = LocalConfiguration.current.screenWidthDp.dp - val snackbarWidth = with(LocalDensity.current) { snackbarSize.toDp() } - - SnackbarHost( - hostState = snackbarHostState, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(start = (width - snackbarWidth).div(2), bottom = TangemTheme.dimens.spacing12) - .fillMaxWidth(), - snackbar = { - Box( - modifier = Modifier - .onSizeChanged { snackbarSize = it.width } - .background( - color = TangemTheme.colors.icon.primary1, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius8), - ) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing14, - ), - ) { - Text( - text = it.visuals.message, - color = TangemTheme.colors.text.primary2, - style = TangemTheme.typography.body2, - ) - } - }, - ) - - val message = stringResource(id = R.string.referral_promo_code_copied) - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = message, - duration = SnackbarDuration.Short, - ) - isCopyButtonPressed.value = false - } - } - } -} - -@Composable -private fun getMessageForErrorSnackbar(errorSnackbar: ErrorSnackbar): String { - return when (errorSnackbar.throwable) { - is DemoModeException -> { - stringResource(id = R.string.alert_demo_feature_disabled) - } - - else -> { - if (errorSnackbar.throwable.cause != null) { - String.format( - format = stringResource(id = R.string.referral_error_failed_to_load_info_with_reason), - errorSnackbar.throwable.cause, - ) - } else { - stringResource(id = R.string.referral_error_failed_to_load_info) - } - } +private fun Resources.getMessageForErrorSnackbar(throwable: Throwable): String { + return when { + throwable is DemoModeException -> getString(R.string.alert_demo_feature_disabled) + throwable.cause != null -> getString(R.string.referral_error_failed_to_load_info_with_reason, throwable.cause) + else -> getString(R.string.referral_error_failed_to_load_info) } } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_Participant_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_ReferralScreen_Participant() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), @@ -475,38 +408,10 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_Participant_InDarkTheme() { - TangemTheme(isDark = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.ParticipantContent( - award = "10 USDT", - networkName = "Tron", - address = "ma80...zk8q2", - discount = "10%", - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - url = "", - expectedAwards = null, - ), - errorSnackbar = ErrorSnackbar(DemoModeException()) {}, - analytics = Analytics( - onAgreementClicked = {}, - onCopyClicked = {}, - onShareClicked = {}, - ), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_ReferralScreen_Participant_With_Referrals() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), @@ -549,9 +454,10 @@ private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_NonParticipant_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_ReferralScreen_NonParticipant() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), @@ -574,53 +480,10 @@ private fun Preview_ReferralScreen_NonParticipant_InLightTheme() { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { - TangemTheme(isDark = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.NonParticipantContent( - award = "10 USDT", - networkName = "Tron", - discount = "10%", - url = "", - onParticipateClicked = {}, - ), - errorSnackbar = null, - analytics = Analytics( - onAgreementClicked = {}, - onCopyClicked = {}, - onShareClicked = {}, - ), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_ReferralScreen_Loading_InLightTheme() { - TangemTheme(isDark = false) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.Loading, - errorSnackbar = null, - analytics = Analytics( - onAgreementClicked = {}, - onCopyClicked = {}, - onShareClicked = {}, - ), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_ReferralScreen_Loading_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_ReferralScreen_Loading() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 4d0d072624..66e103efad 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index da17befee3..1c7d7f9cee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -6,10 +6,10 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter @@ -26,7 +26,7 @@ import javax.inject.Inject internal class SendFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var router: SendRouter diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index fcd33d986f..65318c9c39 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -1,10 +1,13 @@ package com.tangem.features.send.impl.presentation.state +import com.tangem.blockchain.common.Blockchain import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R +import java.math.BigDecimal internal sealed class SendNotification(val config: NotificationConfig) { @@ -162,4 +165,70 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } + + sealed interface Koinos { + data class InsufficientRecoverableMana( + val mana: BigDecimal, + val maxMana: BigDecimal, + ) : Error( + title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title), + subtitle = resourceReference( + R.string.koinos_insufficient_mana_to_send_koin_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), + BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + ), + ), + ) + + data object InsufficientBalance : Error( + title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title), + subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description), + ) + + data class ManaExceedsBalance( + val availableKoinForTransfer: BigDecimal, + val onReduceClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title), + subtitle = resourceReference( + R.string.koinos_mana_exceeds_koin_balance_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmount( + availableKoinForTransfer, + Blockchain.Koinos.currency, + Blockchain.Koinos.decimals(), + ), + ), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)), + onClick = onReduceClick, + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index f2ea95f678..8e552379b5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,18 +1,11 @@ package com.tangem.features.send.impl.presentation.state -import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent -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.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter @@ -20,18 +13,14 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter -import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientWalletListConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber import java.math.BigDecimal -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") internal class SendStateFactory( private val clickIntents: SendClickIntents, private val stateRouterProvider: Provider, @@ -41,7 +30,6 @@ internal class SendStateFactory( private val cryptoCurrencyStatusProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, private val isTapHelpPreviewEnabledProvider: Provider, - private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -72,6 +60,7 @@ internal class SendStateFactory( SendFeeStateConverter( appCurrencyProvider = appCurrencyProvider, feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } private val confirmStateConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -79,14 +68,6 @@ internal class SendStateFactory( isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider, ) } - private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendRecipientWalletListConverter() - } - private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendRecipientHistoryListConverter( - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - ) - } private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) { SendSyncEditConverter(currentStateProvider = currentStateProvider) } @@ -132,156 +113,6 @@ internal class SendStateFactory( } //endregion - //region recipient - fun onLoadedWalletsList(wallets: List): SendUiState { - val state = currentStateProvider() - return state.copy( - recipientState = state.recipientState?.copy( - wallets = recipientWalletListStateConverter.convert(wallets), - ), - ) - } - - fun onLoadedHistoryList(txHistory: List): SendUiState { - val state = currentStateProvider() - return state.copy( - recipientState = state.recipientState?.copy( - recent = recipientHistoryListStateConverter.convert(txHistory), - ), - ) - } - - fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - addressTextField = recipientState.addressTextField.copy(value = value), - memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress), - ), - ) - } - - fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - - val isValidMemo = validateWalletMemoUseCase( - memo = recipientState.memoTextField?.value.orEmpty(), - network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true - - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, - isValidating = false, - addressTextField = recipientState.addressTextField.copy( - error = when { - !isValidAddress -> resourceReference(R.string.send_recipient_address_error) - isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet) - else -> null - }, - isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet, - ), - ), - ) - } - - fun getOnRecipientAddressValidationStarted(): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy(isValidating = true), - ) - } - - fun getOnRecipientMemoValueChange(value: String): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - memoTextField = recipientState.memoTextField?.copy(value = value), - ), - ) - } - - fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - - val isValidMemo = validateWalletMemoUseCase( - memo = value, - network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true - - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, - isValidating = false, - memoTextField = recipientState.memoTextField?.copy( - isError = value.isNotEmpty() && !isValidMemo, - isEnabled = true, - ), - ), - ) - } - - fun getOnXAddressMemoState(): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - memoTextField = recipientState.memoTextField?.copy( - value = "", - isEnabled = false, - ), - ), - ) - } - - fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - val isNotValid = isAddressInWallet || !isValidAddress - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - recent = recipientState.recent.map { recent -> - recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY)) - }.toPersistentList(), - wallets = recipientState.wallets.map { wallet -> - wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY)) - }.toPersistentList(), - ), - ) - } - //endregion - //region send fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() @@ -335,7 +166,8 @@ internal class SendStateFactory( val reducedBy = sendState.reduceAmountBy.takeIf { notifications.none { it is SendNotification.Error.ExistentialDeposit || - it is SendNotification.Error.TransactionLimitError + it is SendNotification.Error.TransactionLimitError || + it is SendNotification.Warning.HighFeeError } } return state.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index c7aa7ddff0..c718256d95 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -71,6 +71,7 @@ internal data class SendUiState( editAmountState = amountState, editFeeState = feeState, editRecipientState = recipientState, + sendState = sendState, ) } else { copy( @@ -125,6 +126,7 @@ internal sealed class SendStates { val feeSelectorState: FeeSelectorState, val fee: Fee?, val rate: BigDecimal?, + val isFeeConvertibleToFiat: Boolean, val appCurrency: AppCurrency, val isFeeApproximate: Boolean, val isCustomSelected: Boolean, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index a79ede8df6..16c1315e2d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,20 +1,24 @@ package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter +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.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents @@ -23,6 +27,7 @@ import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.utils.getFiatString import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -46,6 +51,7 @@ internal class SendNotificationFactory( private val clickIntents: SendClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, ) { fun create(): Flow> = stateRouterProvider().currentState @@ -80,8 +86,9 @@ internal class SendNotificationFactory( addFeeUnreachableNotification(feeState.feeSelectorState) addExceedBalanceNotification(feeValue, sendingAmount) addExceedsBalanceNotification(feeState.fee) - addDustWarningNotification(feeValue, sendingAmount) + addDustWarningNotificationForSpecificBlockchains(feeValue, sendingAmount) addTransactionLimitErrorNotification(feeValue, sendingAmount) + // warnings addExistentialWarningNotification(feeValue, amountValue) addFeeCoverageNotification( @@ -92,6 +99,13 @@ internal class SendNotificationFactory( addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce) addTooHighNotification(feeState.feeSelectorState) addTooLowNotification(feeState) + + // blockchain specific + addValidateTransactionNotifications( + sendingAmount = sendingAmount, + fee = feeState.fee, + state = state, + ) }.toImmutableList() } @@ -280,23 +294,35 @@ internal class SendNotificationFactory( } } - private suspend fun MutableList.addDustWarningNotification( - feeAmount: BigDecimal, - receivedAmount: BigDecimal, + private suspend fun MutableList.addDustWarningNotificationForSpecificBlockchains( + feeValue: BigDecimal, + sendingAmount: BigDecimal, ) { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val dustValue = currencyChecksRepository.getDustValue( - userWalletProvider().walletId, - cryptoCurrencyStatus.currency.network, - ) ?: return + val isCardano = BlockchainUtils.isCardano(cryptoCurrencyStatusProvider().currency.network.id.value) - if (checkDustLimits(feeAmount, receivedAmount, dustValue)) { - add( - SendNotification.Error.MinimumAmountError(dustValue.toPlainString()), - ) + if (!isCardano) { + addDustWarningNotification(feeValue, sendingAmount) } } + private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } + private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return @@ -398,13 +424,123 @@ internal class SendNotificationFactory( return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum } - private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + private suspend fun MutableList.addValidateTransactionNotifications( + sendingAmount: BigDecimal, + fee: Fee?, + state: SendUiState, + ) { + val sendingCurrency = cryptoCurrencyStatusProvider().currency - val totalAmount = feeAmount + receivedAmount - val change = balance - totalAmount - val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO - return receivedAmount < dustValue || isChangeLowerThanDust + validateTransactionUseCase( + amount = sendingAmount.convertToAmount(sendingCurrency), + fee = fee ?: return, + memo = state.recipientState?.memoTextField?.value, + destination = requireNotNull(state.recipientState?.addressTextField?.value), + userWalletId = userWalletProvider().walletId, + network = sendingCurrency.network, + ).fold( + ifLeft = { + when (it) { + is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError( + error = it, + sendingCurrency = sendingCurrency, + ) + is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError(error = it) + else -> return + } + }, + ifRight = { + (fee as? Fee.CardanoToken)?.let { + add( + SendNotification.Cardano.MinAdaValueCharged( + tokenName = sendingCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + }, + ) + } + + private suspend fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> SendNotification.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + val dustValue = currencyChecksRepository.getDustValue( + userWalletId = userWalletProvider().walletId, + network = sendingCurrency.network, + ) ?: return + + add( + SendNotification.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + + private fun MutableList.addKoinosTransactionValidationError(error: BlockchainSdkError.Koinos) { + when (error) { + is BlockchainSdkError.Koinos.InsufficientBalance -> { + add(SendNotification.Koinos.InsufficientBalance) + } + is BlockchainSdkError.Koinos.InsufficientMana -> { + add( + SendNotification.Koinos.InsufficientRecoverableMana( + mana = error.manaBalance ?: BigDecimal.ZERO, + maxMana = error.maxMana ?: BigDecimal.ZERO, + ), + ) + } + is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> { + add( + SendNotification.Koinos.ManaExceedsBalance( + availableKoinForTransfer = error.availableKoinForTransfer, + onReduceClick = { + clickIntents.onAmountReduceClick( + reduceAmountTo = error.availableKoinForTransfer, + clazz = SendNotification.Koinos.InsufficientRecoverableMana::class.java, + ) + }, + ), + ) + } + else -> {} + } + } + + private suspend fun MutableList.addDustWarningNotification( + feeValue: BigDecimal, + sendingAmount: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val dustValue = currencyChecksRepository.getDustValue( + userWalletProvider().walletId, + cryptoCurrencyStatus.currency.network, + ) ?: return + + if (checkDustLimits(feeValue, sendingAmount, dustValue)) { + add( + SendNotification.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index f0cdd28425..d24ed03da0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -10,6 +10,7 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeStateConverter( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: Unit): SendStates.FeeState { @@ -21,6 +22,7 @@ internal class SendFeeStateConverter( appCurrency = appCurrencyProvider(), isFeeApproximate = false, isCustomSelected = false, + isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 3958e3b64a..112b0f1cb7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -50,6 +50,7 @@ internal class SendAmountFieldChangeConverter( val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() return state.copyWrapped( isEditState = isEditState, + sendState = state.sendState?.copy(reduceAmountBy = null), amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance && !isZero, amountTextField = amountTextField.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 8f3d03dbbc..11cfa98faa 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -37,6 +37,7 @@ internal class SendAmountFieldMaxAmountConverter( val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() return state.copyWrapped( isEditState = isEditState, + sendState = state.sendState?.copy(reduceAmountBy = null), amountState = amountState.copy( isPrimaryButtonEnabled = true, amountTextField = amountTextField.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 6cd4f8c31d..4f5ce27038 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -42,6 +42,7 @@ internal sealed class SendTextField { val label: TextReference, val isError: Boolean = false, val error: TextReference? = null, + val isValuePasted: Boolean, ) : SendTextField() data class RecipientMemo( @@ -54,6 +55,7 @@ internal sealed class SendTextField { val error: TextReference? = null, val disabledText: TextReference, val isEnabled: Boolean, + val isValuePasted: Boolean, ) : SendTextField() data class CustomFee( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt index da305b36ea..c024b39ee0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -82,6 +82,7 @@ internal object FeeStatePreviewData { isFeeApproximate = false, notifications = persistentListOf(), isCustomSelected = false, + isFeeConvertibleToFiat = true, ) val feeChoosableState = feeState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt index b4ca6d643a..f3cc308ca1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt @@ -28,6 +28,7 @@ internal object RecipientStatePreviewData { label = stringReference("Recipient"), isError = false, error = null, + isValuePasted = false, ), memoTextField = SendTextField.RecipientMemo( value = "", @@ -39,6 +40,7 @@ internal object RecipientStatePreviewData { error = null, disabledText = stringReference("Already included in the entered address"), isEnabled = true, + isValuePasted = false, ), recent = persistentListOf(), wallets = persistentListOf(), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index 8c08c468e2..2eeda94fd5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -36,7 +36,7 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {} - override fun onRecipientMemoValueChange(value: String) {} + override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {} override fun feeReload() {} @@ -65,7 +65,8 @@ internal object SendClickIntentsStub : SendClickIntents { reduceAmountByDiff: BigDecimal?, reduceAmountTo: BigDecimal?, clazz: Class, - ) {} + ) { + } override fun onNotificationCancel(clazz: Class) {} } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt new file mode 100644 index 0000000000..c3fdbfd71b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt @@ -0,0 +1,195 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class RecipientSendFactory( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val isUtxoConsolidationAvailableProvider: Provider, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, +) { + private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendRecipientWalletListConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + isUtxoConsolidationAvailableProvider = isUtxoConsolidationAvailableProvider, + ) + } + private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendRecipientHistoryListConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + fun onLoadedWalletsList(wallets: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + wallets = recipientWalletListStateConverter.convert(wallets), + ), + ) + } + + fun onLoadedHistoryList(txHistory: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + recent = recipientHistoryListStateConverter.convert(txHistory), + ), + ) + } + + fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false, isValuePasted: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + addressTextField = recipientState.addressTextField.copy(value = value, isValuePasted = isValuePasted), + memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress), + ), + ) + } + + fun getOnRecipientAddressValidState( + value: String, + maybeValidAddress: Either, + ): SendUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + + val isValidMemo = validateWalletMemoUseCase( + memo = recipientState.memoTextField?.value.orEmpty(), + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e("Failed to validateWalletMemoUseCase: $it") + false + } + + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && maybeValidAddress.isRight(), + isValidating = false, + addressTextField = recipientState.addressTextField.copy( + error = maybeValidAddress.fold( + ifLeft = { + when (it) { + ValidateAddressError.InvalidAddress -> resourceReference( + R.string.send_recipient_address_error, + ) + ValidateAddressError.AddressInWallet -> resourceReference( + R.string.send_error_address_same_as_wallet, + ) + else -> null + } + }, + ifRight = { null }, + ), + isError = value.isNotEmpty() && maybeValidAddress.isLeft(), + ), + ), + ) + } + + fun getOnRecipientAddressValidationStarted(): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy(isValidating = true), + ) + } + + fun getOnRecipientMemoValueChange(value: String, isValuePasted: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + memoTextField = recipientState.memoTextField?.copy( + value = value, + isValuePasted = isValuePasted, + ), + ), + ) + } + + fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + + val isValidMemo = validateWalletMemoUseCase( + memo = value, + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e("Failed to validateWalletMemoUseCase: $it") + false + } + + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress, + isValidating = false, + memoTextField = recipientState.memoTextField?.copy( + isError = value.isNotEmpty() && !isValidMemo, + isEnabled = true, + ), + ), + ) + } + + fun getOnXAddressMemoState(): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + memoTextField = recipientState.memoTextField?.copy( + value = "", + isEnabled = false, + ), + ), + ) + } + + fun getHiddenRecentListState(isNotValid: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + recent = recipientState.recent.map { recent -> + recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY)) + }.toPersistentList(), + wallets = recipientState.wallets.map { wallet -> + wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY)) + }.toPersistentList(), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt index ab822de809..2365bc31e2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt @@ -24,6 +24,7 @@ internal class SendRecipientAddressFieldConverter( error = resourceReference(R.string.send_recipient_address_error), placeholder = resourceReference(R.string.send_enter_address_field), label = resourceReference(R.string.send_recipient), + isValuePasted = false, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index a539d72859..1525d0d6ab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -58,6 +58,7 @@ internal class SendRecipientMemoFieldConverter( error = resourceReference(R.string.send_memo_destination_tag_error), disabledText = resourceReference(R.string.send_additional_field_already_included), isEnabled = true, + isValuePasted = false, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt index 35d56f9846..6ac1a69d3f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt @@ -1,16 +1,22 @@ package com.tangem.features.send.impl.presentation.state.recipient import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_DEFAULT_COUNT import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_KEY_TAG import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList -internal class SendRecipientWalletListConverter : +internal class SendRecipientWalletListConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val isUtxoConsolidationAvailableProvider: Provider, +) : Converter, PersistentList> { override fun convert(value: List): PersistentList { return value.filterWallets().ifEmpty { @@ -20,8 +26,18 @@ internal class SendRecipientWalletListConverter : private fun List.filterWallets(): PersistentList { var walletsCounter = 0 + val currentAddress: String = runCatching { + cryptoCurrencyStatusProvider().value.networkAddress?.defaultAddress?.value + }.getOrNull().orEmpty() + return this.filterNotNull() - .filter { it.address.isNotBlank() } + .filter { + val isCoin = it.cryptoCurrency is CryptoCurrency.Coin + val isNotSameAddress = it.address != currentAddress + val isNotBlankAddress = it.address.isNotBlank() + + isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailableProvider()) + } .groupBy { item -> item.name } .values.map { wallets -> val groupedByWallet = wallets.groupBy { it.userWalletId } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 3f5d13ae28..f5247e516a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -29,16 +29,15 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.shareText -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.Amount import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.utils.getFiatFormatted import com.tangem.features.send.impl.presentation.utils.getFiatString +import com.tangem.features.send.impl.presentation.utils.getCryptoReference @Composable internal fun SendNavigationButtons( @@ -175,23 +174,36 @@ private fun SendingText( val sendingFiat = if (uiState.isSubtracted) { fiatAmount?.value } else { - feeFiat?.let { fiatAmount?.value?.plus(it) } + if (feeState?.isFeeConvertibleToFiat == true) { + feeFiat?.let { fiatAmount?.value?.plus(it) } + } else { + fiatAmount?.value + } } if (feeFiat != null && sendingFiat != null) { - val sendingValue = getFiatFormatted( - value = sendingFiat, - currencySymbol = feeState.appCurrency.symbol, - currencyCode = feeState.appCurrency.code, - ) - val feeValue = getFiatString( - value = feeState.fee?.amount?.value, - rate = feeState.rate, - appCurrency = feeState.appCurrency, + val sendingValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = sendingFiat, + fiatCurrencySymbol = feeState.appCurrency.symbol, + fiatCurrencyCode = feeState.appCurrency.code, ) + val feeValue = if (feeState.isFeeConvertibleToFiat) { + getFiatString( + value = feeState.fee?.amount?.value, + rate = feeState.rate, + appCurrency = feeState.appCurrency, + ) + } else { + getCryptoReference(feeState.fee?.amount, feeState.isFeeApproximate)?.resolveReference().orEmpty() + } + val textResource = remember(uiState) { resourceReference( - id = R.string.send_summary_transaction_description, + id = if (feeState.isFeeConvertibleToFiat) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, formatArgs = wrappedList(sendingValue, feeValue), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 819655152a..cdef0b59c6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope @@ -12,15 +13,21 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* 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.PreviewParameterProvider import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.extensions.resolveReference 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.res.TangemThemePreview import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData +import com.tangem.features.send.impl.presentation.state.previewdata.SendStatesPreviewData import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent @@ -182,4 +189,49 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS } } } -} \ No newline at end of file +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 736) +@Preview(showBackground = true, widthDp = 360, heightDp = 736, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SendScreen_Preview(@PreviewParameter(SendScreenPreviewProvider::class) data: SendScreenPreview) { + TangemThemePreview { + SendScreen( + uiState = data.uiState, + currentState = data.currentState, + ) + } +} + +private class SendScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SendScreenPreview( + uiState = SendStatesPreviewData.uiState, + currentState = SendUiCurrentScreen(type = SendUiStateType.Recipient, isFromConfirmation = false), + ), + SendScreenPreview( + uiState = SendStatesPreviewData.uiState, + currentState = SendUiCurrentScreen(type = SendUiStateType.Amount, isFromConfirmation = false), + ), + SendScreenPreview( + uiState = SendStatesPreviewData.uiState, + currentState = SendUiCurrentScreen(type = SendUiStateType.Send, isFromConfirmation = false), + ), + SendScreenPreview( + uiState = SendStatesPreviewData.uiState, + currentState = SendUiCurrentScreen(type = SendUiStateType.EditFee, isFromConfirmation = true), + ), + SendScreenPreview( + uiState = SendStatesPreviewData.uiState.copy(sendState = ConfirmStatePreviewData.sendDoneState), + currentState = SendUiCurrentScreen(type = SendUiStateType.Send, isFromConfirmation = false), + ), + ) +} + +private data class SendScreenPreview( + val uiState: SendUiState, + val currentState: SendUiCurrentScreen, +) +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt index acb6e230a0..19325703a5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt @@ -94,19 +94,20 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, i horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { + val iconModifier = Modifier.size(TangemTheme.dimens.size18) + .padding(horizontal = TangemTheme.dimens.spacing1) if (button.isFiat) { FiatIcon( url = button.iconUrl, size = TangemTheme.dimens.size18, isGrayscale = !isSegmentedButtonsEnabled, - modifier = Modifier.size(TangemTheme.dimens.size18), + modifier = iconModifier, ) } else if (button.iconState != null) { TokenIcon( state = button.iconState, shouldDisplayNetwork = false, - modifier = Modifier - .size(TangemTheme.dimens.size18), + modifier = iconModifier, ) } Text( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 4452c08cb6..6181fc2174 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.amount +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -8,6 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData @@ -42,12 +44,13 @@ internal fun SendAmountContent( } // region Preview -@Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AmountFieldPreview_Light( - @PreviewParameter(AmountStatePreviewProvider::class) amountState: SendStates.AmountState, +private fun SendAmountContentPreview( + @PreviewParameter(SendAmountContentPreviewProvider::class) amountState: SendStates.AmountState, ) { - TangemTheme { + TangemThemePreview { SendAmountContent( amountState = amountState, isBalanceHiding = false, @@ -56,21 +59,7 @@ private fun AmountFieldPreview_Light( } } -@Preview -@Composable -private fun AmountFieldPreview_Dark( - @PreviewParameter(AmountStatePreviewProvider::class) amountState: SendStates.AmountState, -) { - TangemTheme(isDark = true) { - SendAmountContent( - amountState = amountState, - isBalanceHiding = false, - clickIntents = SendClickIntentsStub, - ) - } -} - -private class AmountStatePreviewProvider : PreviewParameterProvider { +private class SendAmountContentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( AmountStatePreviewData.amountState, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index 4b6919dd3e..dedf7fa608 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.fee +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxWidth @@ -8,10 +9,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData +import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub import com.tangem.features.send.impl.presentation.ui.common.notifications import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents @@ -75,4 +82,30 @@ internal fun LazyListScope.customFee( .padding(top = TangemTheme.dimens.spacing12), ) } -} \ No newline at end of file +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SendSpeedAndFeeContent_Preview( + @PreviewParameter(FeeStatePreviewProvider::class) feeState: SendStates.FeeState, +) { + TangemThemePreview { + SendSpeedAndFeeContent( + state = feeState, + clickIntents = SendClickIntentsStub, + ) + } +} + +private class FeeStatePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + FeeStatePreviewData.feeState, + FeeStatePreviewData.feeChoosableState, + FeeStatePreviewData.feeCustomState, + FeeStatePreviewData.errorFeeState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index 270d4954f9..f403cf423b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.fee +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeType @@ -107,21 +109,12 @@ private fun FooterText(onReadMoreClick: () -> Unit) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SendSpeedSelectorPreview_Light( +private fun SendSpeedSelectorPreview( @PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState, ) { - TangemTheme { - SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub) - } -} - -@Preview -@Composable -private fun SendSpeedSelectorPreview_Dark( - @PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index d29fea2394..ed1ad443a4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -54,7 +54,11 @@ internal fun SendSpeedSelectorItem( onSelect = onSelect, modifier = modifier, preDot = getCryptoReference(amount, state.isFeeApproximate), - postDot = getFiatReference(amount?.value, state.rate, state.appCurrency), + postDot = if (state.isFeeConvertibleToFiat) { + getFiatReference(amount?.value, state.rate, state.appCurrency) + } else { + null + }, ellipsizeOffset = amount?.currencySymbol?.length, isSelected = content?.selectedFee == feeType, showDivider = showDivider, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index 92c49f2dff..ec3e048834 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.recipient +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R @@ -183,28 +185,12 @@ private fun ListItemLoading(modifier: Modifier = Modifier) { // region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ListItemWithIconPreview_Light( +private fun ListItemWithIconPreview( @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, ) { - TangemTheme { - ListItemWithIcon( - title = config.title, - subtitle = config.subtitle, - subtitleEndOffset = config.subtitleEndOffset, - subtitleIconRes = config.iconRes, - onClick = {}, - isLoading = config.isLoading, - ) - } -} - -@Preview -@Composable -private fun ListItemWithIconPreview_Dark( - @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ListItemWithIcon( title = config.title, subtitle = config.subtitle, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 7885c8b3ad..b3402a2bbe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -25,6 +25,7 @@ import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent @@ -70,7 +71,7 @@ internal fun SendRecipientContent( ) memoField( memoField = memoField, - onMemoChange = clickIntents::onRecipientMemoValueChange, + onMemoChange = { clickIntents.onRecipientMemoValueChange(it, true) }, ) listHeaderItem( titleRes = R.string.send_recipient_wallets_title, @@ -117,6 +118,7 @@ private fun LazyListScope.addressItem( isError = isError, isLoading = isValidating, error = address.error, + isValuePasted = address.isValuePasted, modifier = Modifier .background( color = TangemTheme.colors.background.action, @@ -143,6 +145,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM isError = memoField.isError, error = memoField.error, isReadOnly = !memoField.isEnabled, + isValuePasted = memoField.isValuePasted, ) } } @@ -258,7 +261,7 @@ private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () private fun SendRecipientContent_Preview( @PreviewParameter(SendRecipientContentPreviewProvider::class) recipientState: SendStates.RecipientState, ) { - TangemTheme(isDark = false) { + TangemThemePreview { SendRecipientContent( uiState = recipientState, clickIntents = SendClickIntentsStub, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index eeed6b1e9b..e11b62e872 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -32,6 +32,7 @@ internal fun TextFieldWithPaste( error: TextReference? = null, isError: Boolean = false, isReadOnly: Boolean = false, + isValuePasted: Boolean = false, ) { val (title, color) = when { isError && error != null -> error to TangemTheme.colors.text.warning @@ -65,6 +66,7 @@ internal fun TextFieldWithPaste( placeholderColor = placeholderColor, onValueChange = onValueChange, readOnly = isReadOnly, + isValuePasted = isValuePasted, modifier = Modifier .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing8), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index b3b5d066d2..a4912c792e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -16,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendStates @@ -54,10 +56,7 @@ internal fun AmountBlock( .clip(TangemTheme.shapes.roundedCornersXMedium) .background(backgroundColor) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) - .padding( - vertical = TangemTheme.dimens.spacing14, - horizontal = TangemTheme.dimens.spacing16, - ), + .padding(TangemTheme.dimens.spacing16), ) { TokenIcon(state = amountState.tokenIconState) ResizableText( @@ -77,21 +76,17 @@ internal fun AmountBlock( textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing2, - ), + .padding(top = TangemTheme.dimens.spacing8), ) } } // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AmountBlockPreview_Light( - @PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState, -) { - TangemTheme { +private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState) { + TangemThemePreview { AmountBlock( amountState = value, isClickDisabled = false, @@ -101,21 +96,6 @@ private fun AmountBlockPreview_Light( } } -@Preview -@Composable -private fun AmountBlockPreview_Dark( - @PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState, -) { - TangemTheme(isDark = true) { - AmountBlock( - amountState = value, - isClickDisabled = true, - isEditingDisabled = false, - onClick = {}, - ) - } -} - private class AmountBlockPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index d0fe044682..f2dcd33987 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R @@ -60,7 +62,11 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o titleRes = title, iconRes = icon, preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), - postDot = getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency), + postDot = if (feeState.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) + } else { + null + }, ellipsizeOffset = feeAmount?.currencySymbol?.length, isSelected = true, showDivider = false, @@ -111,21 +117,10 @@ private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun FeeBlockPreview_Light(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { - TangemTheme { - FeeBlock( - feeState = value, - isClickDisabled = true, - onClick = {}, - ) - } -} - -@Preview -@Composable -private fun FeeBlockPreview_Dark(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { - TangemTheme(isDark = true) { +private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { + TangemThemePreview { FeeBlock( feeState = value, isClickDisabled = true, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 3936e4985b..4839b83690 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendTextField @@ -97,26 +99,12 @@ private fun MemoBlock(memo: SendTextField.RecipientMemo?) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun RecipientBlockPreview_Light( +private fun RecipientBlockPreview( @PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState, ) { - TangemTheme { - RecipientBlock( - recipientState = value, - isClickDisabled = true, - isEditingDisabled = false, - onClick = {}, - ) - } -} - -@Preview -@Composable -private fun RecipientBlockPreview_Dark( - @PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { RecipientBlock( recipientState = value, isClickDisabled = true, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 3c7716a64c..0f6304ae81 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.ExperimentalFoundationApi @@ -20,10 +21,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData +import com.tangem.features.send.impl.presentation.state.previewdata.SendStatesPreviewData import com.tangem.features.send.impl.presentation.ui.common.notifications import kotlinx.coroutines.delay @@ -133,4 +140,25 @@ private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modif } } } -} \ No newline at end of file +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SendContent_Preview(@PreviewParameter(SendContentPreviewProvider::class) uiState: SendUiState) { + TangemThemePreview { + SendContent( + uiState = uiState, + ) + } +} + +private class SendContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SendStatesPreviewData.uiState, + SendStatesPreviewData.uiState.copy(sendState = ConfirmStatePreviewData.sendDoneState), + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt index f2be7e6ed2..7c8a2becb6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -8,11 +8,8 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal -import java.math.RoundingMode -private const val FIAT_DECIMALS = 2 private const val CRYPTO_FEE_DECIMALS = 6 -private const val FEE_MINIMUM_VALUE = 0.01 internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { if (amount == null) return null @@ -37,27 +34,9 @@ internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) - return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol) -} - -internal fun getFiatFormatted(value: BigDecimal?, currencyCode: String, currencySymbol: String): String { - val scaled = value?.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO - return if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { - buildString { - append(BigDecimalFormatter.CAN_BE_LOWER_SIGN) - append( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = BigDecimal(FEE_MINIMUM_VALUE), - fiatCurrencyCode = currencyCode, - fiatCurrencySymbol = currencySymbol, - ), - ) - } - } else { - BigDecimalFormatter.formatFiatAmount( - fiatAmount = value, - fiatCurrencyCode = currencyCode, - fiatCurrencySymbol = currencySymbol, - ) - } + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 8eb2a331d5..457de01af1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -39,7 +39,7 @@ internal interface SendClickIntents { // region Recipient fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null) - fun onRecipientMemoValueChange(value: String) + fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false) // endregion // region Fee diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 40add3e136..456d91b7c6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -4,10 +4,10 @@ import android.os.SystemClock import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.util.fastDistinctBy import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse +import arrow.core.left import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -30,18 +30,14 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase -import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -53,12 +49,10 @@ import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory import com.tangem.features.send.impl.presentation.state.fee.* +import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendFactory import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.DelayedWork -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @@ -74,11 +68,10 @@ import kotlin.properties.Delegates internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, @@ -99,10 +92,12 @@ internal class SendViewModel @Inject constructor( private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, @DelayedWork private val coroutineScope: CoroutineScope, + validateTransactionUseCase: ValidateTransactionUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, - validateWalletMemoUseCase: ValidateWalletMemoUseCase, getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -133,10 +128,17 @@ internal class SendViewModel @Inject constructor( appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, - validateWalletMemoUseCase = validateWalletMemoUseCase, isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled }, ) + private val recipientStateFactory = RecipientSendFactory( + stateRouterProvider = Provider { stateRouter }, + currentStateProvider = Provider { uiState }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable }, + validateWalletMemoUseCase = validateWalletMemoUseCase, + ) + private val amountStateFactory = AmountStateFactory( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, @@ -178,6 +180,7 @@ internal class SendViewModel @Inject constructor( clickIntents = this, analyticsEventHandler = analyticsEventHandler, getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + validateTransactionUseCase = validateTransactionUseCase, ) private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { @@ -196,6 +199,7 @@ internal class SendViewModel @Inject constructor( private var userWallet: UserWallet by Delegates.notNull() private var userWallets: List = emptyList() private var isAmountSubtractAvailable: Boolean = false + private var isUtxoConsolidationAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -241,11 +245,12 @@ internal class SendViewModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet checkIfSubtractAvailable() + checkIfUtxoConsolidationAvailable() val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() val isMultiCurrency = wallet.isMultiCurrency @@ -255,9 +260,7 @@ internal class SendViewModel @Inject constructor( ) }, ifLeft = { - uiState = eventStateFactory.getGenericErrorState( - onConsume = { uiState = eventStateFactory.onConsumeEventState() }, - ) + showErrorAlert() return@launch }, ) @@ -275,73 +278,62 @@ internal class SendViewModel @Inject constructor( .saveIn(balanceHidingJobHolder) } - // TODO [REDACTED_JIRA] - private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - if (cryptoCurrency is CryptoCurrency.Coin) { - getCurrencyStatusUpdates( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ).onEach { currencyStatus -> - currencyStatus.onRight { - onDataLoaded( - currencyStatus = it, - coinCurrencyStatus = it, - feeCurrencyStatus = getFeeCurrencyStatusSync(it, isMultiCurrency), - ) - } - } - .flowOn(dispatchers.main) - .launchIn(viewModelScope) - .saveIn(balanceJobHolder) + private suspend fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { + val maybeCurrencyStatus = getCurrencyStatus( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ) + val maybeCoinStatus = if (cryptoCurrency is CryptoCurrency.Coin) { + maybeCurrencyStatus } else { - combine( - flow = getCoinCurrencyStatusUpdates(isSingleWalletWithToken), - flow2 = getCurrencyStatusUpdates( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ), - ) { maybeCoinStatus, maybeCurrencyStatus -> - if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { - val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") } - val coinStatus = maybeCoinStatus.getOrElse { error("Coin status is unreachable") } - onDataLoaded( - currencyStatus = currencyStatus, - coinCurrencyStatus = coinStatus, - feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), - ) - } + getCoinCurrencyStatusUpdates(isSingleWalletWithToken) + } + + if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { + val currencyStatus = maybeCurrencyStatus.getOrElse { + showErrorAlert() + return Timber.e("Currency status is unreachable") } - .flowOn(dispatchers.main) - .launchIn(viewModelScope) - .saveIn(balanceJobHolder) + val coinStatus = maybeCoinStatus.getOrElse { + showErrorAlert() + return Timber.e("Coin status is unreachable") + } + onDataLoaded( + currencyStatus = currencyStatus, + coinCurrencyStatus = coinStatus, + feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), + ) + } else { + showErrorAlert() } } private fun getTapHelpPreviewAvailability() { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { isTapHelpPreviewEnabled = isSendTapHelpEnabledUseCase().getOrElse { false } } } - private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase( - userWalletId = userWalletId, - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = isSingleWalletWithToken, - ).conflate().distinctUntilChanged() + private suspend fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase + .invokeSync( + userWalletId = userWalletId, + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = isSingleWalletWithToken, + ) - private fun getCurrencyStatusUpdates( + private suspend fun getCurrencyStatus( isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean, - ): Flow> { + ): Either { return if (isMultiCurrency) { - getCurrencyStatusUpdatesUseCase( + getCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, - currencyId = cryptoCurrency.id, + cryptoCurrencyId = cryptoCurrency.id, isSingleWalletWithTokens = isSingleWalletWithToken, - ).conflate().distinctUntilChanged() + ) } else { - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + getCryptoCurrencyStatusSyncUseCase(userWalletId = userWalletId) } } @@ -399,30 +391,28 @@ internal class SendViewModel @Inject constructor( private fun getWalletsAndRecent() { getUserWallets() - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { getTxHistory() } } private fun getUserWallets() { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { runCatching { - getWalletsUseCase.invokeSync() - ?.toAvailableWallets() - .orEmpty() + waitForDelay(delay = RECENT_LOAD_DELAY) { + getWalletsUseCase.invokeSync() + .toAvailableWallets() + } }.onSuccess { result -> userWallets = result - uiState = stateFactory.onLoadedWalletsList(wallets = userWallets) + uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets) }.onFailure { - uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) + uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList()) } } } private suspend fun List.toAvailableWallets(): List { - val currentAddress: String = kotlin.runCatching { - cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - }.getOrNull().orEmpty() return filterNot { it.isLocked } .mapNotNull { wallet -> val addresses = if (!wallet.isMultiCurrency) { @@ -436,26 +426,26 @@ internal class SendViewModel @Inject constructor( } else { getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrency.network) } - addresses - ?.filter { it.address != currentAddress } - ?.map { (cryptoCurrency, address) -> - AvailableWallet( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrency, - userWalletId = wallet.walletId, - ) - }?.fastDistinctBy { it.address } + addresses?.map { (cryptoCurrency, address) -> + AvailableWallet( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrency, + userWalletId = wallet.walletId, + ) + } }.flatten() } private suspend fun getTxHistory() { - val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( - userWalletId = userWalletId, - currency = cryptoCurrency, - pageSize = RECENT_TX_SIZE, - ).getOrElse { emptyList() } - uiState = stateFactory.onLoadedHistoryList(txHistory = txHistoryList) + val txHistoryList = waitForDelay(delay = RECENT_LOAD_DELAY) { + getFixedTxHistoryItemsUseCase.getSync( + userWalletId = userWalletId, + currency = cryptoCurrency, + pageSize = RECENT_TX_SIZE, + ).getOrElse { emptyList() } + } + uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList) } private fun onStateActive() { @@ -592,7 +582,7 @@ internal class SendViewModel @Inject constructor( } private fun cancelFeeRequest() { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { feeJobHolder.cancel() } } @@ -613,7 +603,7 @@ internal class SendViewModel @Inject constructor( ) } - // endregion +// endregion // region amount state clicks override fun onCurrencyChangeClick(isFiat: Boolean) { @@ -639,59 +629,71 @@ internal class SendViewModel @Inject constructor( override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = stateFactory.onRecipientAddressValueChange(value) - uiState = stateFactory.getOnRecipientAddressValidationStarted() + uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null) + uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() val isValidAddress = validateAddress(value) - uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress) - type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) } - autoNextFromRecipient(type, isValidAddress) + uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) + type?.let { + analyticsEventHandler.send( + SendAnalyticEvents.AddressEntered( + it, + isValidAddress.isRight(), + ), + ) + } + autoNextFromRecipient(type, isValidAddress.isRight()) } }.saveIn(addressValidationJobHolder) } - override fun onRecipientMemoValueChange(value: String) { + override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = stateFactory.getOnRecipientMemoValueChange(value) - uiState = stateFactory.getOnRecipientAddressValidationStarted() - val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty()) - uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress) + uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) + uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() + val recipientState = uiState.getRecipientState(stateRouter.isEditState) + val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty()) + uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight()) } }.saveIn(memoValidationJobHolder) } - private suspend fun validateAddress(value: String): Boolean = runCatching { - val isValidAddress = validateWalletAddressUseCase( + private suspend fun validateAddress(value: String): Either = runCatching { + val maybeValidAddress = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, address = value, - ).getOrElse { false } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true - onEnteredValidAddress(isValidAddress, isAddressInWallet) - return isValidAddress - }.getOrElse { false } + currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses, + ) + onEnteredValidAddress(maybeValidAddress.isLeft()) + maybeValidAddress + }.getOrElse { ValidateAddressError.DataError(it).left() } + + private fun validateMemo(value: String?): Boolean { + return value?.let { validateWalletMemoUseCase(cryptoCurrency.network, it).getOrElse { false } } ?: true + } private suspend fun checkIfXrpAddressValue(value: String): Boolean { return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> - uiState = stateFactory.onRecipientAddressValueChange(value, isXAddress = true) - uiState = stateFactory.getOnXAddressMemoState() + uiState = + recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true) + uiState = recipientStateFactory.getOnXAddressMemoState() val isValidAddress = validateAddress(decodedAddress.address) - uiState = stateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) + uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) true } ?: false } - private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) { - uiState = stateFactory.getHiddenRecentListState( - isAddressInWallet = isAddressInWallet, - isValidAddress = isValidAddress, - ) + private fun onEnteredValidAddress(isNotValid: Boolean) { + uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid) } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { + val memo = uiState.getRecipientState(stateRouter.isEditState)?.memoTextField?.value + val isValidMemo = validateMemo(memo) + val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress) onNextClick(stateRouter.isEditState) + if (isRecent && isValidAddress && isValidMemo) onNextClick(stateRouter.isEditState) } // endregion @@ -722,7 +724,7 @@ internal class SendViewModel @Inject constructor( } private fun loadFee() { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { val isShowStatus = uiState.feeState?.fee == null if (isShowStatus) { uiState = feeStateFactory.onFeeOnLoadingState() @@ -756,6 +758,13 @@ internal class SendViewModel @Inject constructor( ) } + private suspend fun checkIfUtxoConsolidationAvailable() { + isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + } + private suspend fun callFeeUseCase(): Either? { val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation val amountState = uiState.getAmountState(isFromConfirmation) ?: return null @@ -860,7 +869,7 @@ internal class SendViewModel @Inject constructor( reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO, ) - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { createTransactionUseCase( amount = receivingAmount.convertToAmount(cryptoCurrency), fee = fee, @@ -914,10 +923,14 @@ internal class SendViewModel @Inject constructor( val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return val destinationAddress = recipientState.addressTextField.value - val maybeUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return + val receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return - viewModelScope.launch(dispatchers.io) { - addCryptoCurrenciesUseCase(userWalletId = maybeUserWallet.userWalletId, currency = cryptoCurrency) + viewModelScope.launch { + addCryptoCurrenciesUseCase( + userWalletId = receivingUserWallet.userWalletId, + cryptoCurrency = cryptoCurrency, + network = receivingUserWallet.cryptoCurrency.network, + ) } } @@ -949,7 +962,7 @@ internal class SendViewModel @Inject constructor( val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error } if (!isSuccess && noErrorNotifications) { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { val feeUpdatedState = callFeeUseCase()?.fold( ifRight = { uiState = stateFactory.getSendingStateUpdate(isSending = false) @@ -983,16 +996,23 @@ internal class SendViewModel @Inject constructor( } private fun setNeverToShowTapHelp() { - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { neverShowTapHelpUseCase() } uiState = stateFactory.getHiddenTapHelpState() } + + private fun showErrorAlert() { + uiState = eventStateFactory.getGenericErrorState( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + } // endregion private companion object { const val CHECK_FEE_UPDATE_DELAY = 60_000L const val BALANCE_UPDATE_DELAY = 11_000L + const val RECENT_LOAD_DELAY = 500L const val RECENT_TX_SIZE = 100 const val RU_LOCALE = "ru" diff --git a/features/staking/api/.gitignore b/features/staking/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/staking/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/staking/api/build.gradle.kts b/features/staking/api/build.gradle.kts new file mode 100644 index 0000000000..a14e2d42b0 --- /dev/null +++ b/features/staking/api/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.staking.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt new file mode 100644 index 0000000000..90ccdc617d --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.staking.api.featuretoggles + +/** + * Staking feature toggles + */ +interface StakingFeatureToggles { + + /** Availability of staking */ + val isStakingEnabled: Boolean +} \ No newline at end of file diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt new file mode 100644 index 0000000000..d2b216513e --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.api.navigation + +import androidx.fragment.app.Fragment + +interface StakingRouter { + + fun getEntryFragment(): Fragment +} \ No newline at end of file diff --git a/features/staking/impl/.gitignore b/features/staking/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/staking/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts new file mode 100644 index 0000000000..ac2ce74301 --- /dev/null +++ b/features/staking/impl/build.gradle.kts @@ -0,0 +1,62 @@ +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.staking.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.androidx.paging.runtime) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.material) + implementation(deps.arrow.core) + implementation(deps.lifecycle.compose) + implementation(deps.jodatime) + implementation(deps.timber) + + /** 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) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + implementation(deps.compose.constraintLayout) + + /** Tangem SDKs */ + implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) + + /** Core modules */ + implementation(projects.core.featuretoggles) + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.navigation) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + + /** Common */ + implementation(projects.common) + + /** Libs */ + implementation(projects.libs.crypto) + + /** Feature modules */ + implementation(projects.features.staking.api) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt new file mode 100644 index 0000000000..e40072f8bf --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.features.staking.impl.featuretoggles.DefaultStakingFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * DI module provides implementation of [StakingFeatureToggles] + */ +@Module +@InstallIn(SingletonComponent::class) +internal object StakingFeatureTogglesModule { + + @Provides + @Singleton + fun provideStakingFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { + return DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt new file mode 100644 index 0000000000..c351d87a6e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -0,0 +1,23 @@ +package com.tangem.features.staking.impl.di + +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter +import com.tangem.features.staking.api.navigation.StakingRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [StakingRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +internal object StakingRouterModule { + + @Provides + @ActivityScoped + fun provideStakingRouter(): StakingRouter { + return DefaultStakingRouter() + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt new file mode 100644 index 0000000000..735c22b027 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles + +/** + * Default implementation of staking feature toggles + * + * @property featureTogglesManager manager for getting information about the availability of feature toggles + */ +internal class DefaultStakingFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : StakingFeatureToggles { + + override val isStakingEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_ENABLED") +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt new file mode 100644 index 0000000000..83cd830e4d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.impl.navigation + +import androidx.fragment.app.Fragment + +internal class DefaultStakingRouter : InnerStakingRouter { + + override fun getEntryFragment(): Fragment = TODO() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt new file mode 100644 index 0000000000..7d7aa226d4 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -0,0 +1,5 @@ +package com.tangem.features.staking.impl.navigation + +import com.tangem.features.staking.api.navigation.StakingRouter + +interface InnerStakingRouter : StakingRouter \ No newline at end of file diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 208635ebbd..5998ddafd0 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { /** Domain */ implementation(projects.domain.tokens.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) 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 c67891b454..edd4767043 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 @@ -8,11 +8,13 @@ import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails import com.tangem.datasource.api.express.models.response.SwapPair @@ -20,11 +22,10 @@ import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.crypto.DataSignatureVerifier -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -48,7 +49,7 @@ internal class DefaultSwapRepository @Inject constructor( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val walletsStateHolder: WalletsStateHolder, + private val userWalletsListManager: UserWalletsListManager, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, moshi: Moshi, @@ -249,6 +250,7 @@ internal class DefaultSwapRepository @Inject constructor( fromContractAddress: String, fromNetwork: String, toContractAddress: String, + fromAddress: String, toNetwork: String, fromAmount: String, fromDecimals: Int, @@ -266,6 +268,7 @@ internal class DefaultSwapRepository @Inject constructor( fromContractAddress = fromContractAddress, fromNetwork = fromNetwork, toContractAddress = toContractAddress, + fromAddress = fromAddress, toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, @@ -301,6 +304,31 @@ internal class DefaultSwapRepository @Inject constructor( } } + override suspend fun exchangeSent( + txId: String, + fromNetwork: String, + fromAddress: String, + payInAddress: String, + txHash: String, + payInExtraId: String?, + ): Either = withContext(coroutineDispatcher.io) { + try { + tangemExpressApi.exchangeSent( + ExchangeSentRequestBody( + txId = txId, + fromNetwork = fromNetwork, + fromAddress = fromAddress, + payinAddress = payInAddress, + payinExtraId = payInExtraId, + txHash = txHash, + ), + ).getOrThrow() + Unit.right() + } catch (ex: Exception) { + getDataError(ex).left() + } + } + private fun parseTxDetails(txDetailsJson: String): TxDetails? { return try { txDetailsMoshiAdapter.fromJson(txDetailsJson) @@ -389,8 +417,8 @@ internal class DefaultSwapRepository @Inject constructor( blockchain = blockchain, extraDerivationPath = null, derivationStyleProvider = requireNotNull( - walletsStateHolder.userWalletsListManager - ?.selectedUserWalletSync + userWalletsListManager + .selectedUserWalletSync ?.scanResponse ?.derivationStyleProvider, ), 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 e360f8bfad..7cca09753d 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 @@ -11,12 +11,16 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.SavedSwapTransactionListConverter import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, ) : SwapTransactionRepository { private val converter = SavedSwapTransactionListConverter() @@ -70,28 +74,30 @@ class DefaultSwapTransactionRepository( cryptoCurrencyId: CryptoCurrency.ID, scanResponse: ScanResponse, ): Flow?> { - val txStatuses = appPreferencesStore.getObjectMap( - key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, - ) - return appPreferencesStore.getObjectList( - key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, - ).map { savedTransactions -> - val currencyTxs = savedTransactions - ?.filter { - it.userWalletId == userWalletId.stringValue && - ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value - ) - } + return withContext(dispatchers.io) { + val txStatuses = appPreferencesStore.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + appPreferencesStore.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ).map { savedTransactions -> + val currencyTxs = savedTransactions + ?.filter { + it.userWalletId == userWalletId.stringValue && + ( + it.toCryptoCurrencyId == cryptoCurrencyId.value || + it.fromCryptoCurrencyId == cryptoCurrencyId.value + ) + } - currencyTxs?.mapNotNull { - converter.convertBack( - value = it, - scanResponse = scanResponse, - txStatuses = txStatuses, - ) - } + currencyTxs?.mapNotNull { + converter.convertBack( + value = it, + scanResponse = scanResponse, + txStatuses = txStatuses, + ) + } + }.flowOn(dispatchers.io) } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt index e3c5321be7..0a566d5500 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -10,7 +10,7 @@ internal class ExchangeStatusConverter : Converter + // TODO: Add target error handling, remove either ([REDACTED_JIRA]) + @Suppress("LongParameterList") + suspend fun exchangeSent( + txId: String, + fromNetwork: String, + fromAddress: String, + payInAddress: String, + txHash: String, + payInExtraId: String?, + ): Either + fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index e0cb9a413a..73348d6d0b 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.demo) implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) @@ -46,5 +47,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.timber) implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) implementation(deps.moshi) } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt index a1c0981569..45473c85a9 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -8,12 +8,14 @@ sealed class ExpressTransactionModel { abstract val toAmount: SwapAmount abstract val txId: String abstract val txTo: String + abstract val txExtraId: String? data class DEX( override val fromAmount: SwapAmount, override val toAmount: SwapAmount, override val txId: String, override val txTo: String, + override val txExtraId: String?, val txFrom: String, val txData: String, ) : ExpressTransactionModel() @@ -23,9 +25,9 @@ sealed class ExpressTransactionModel { override val toAmount: SwapAmount, override val txId: String, override val txTo: String, + override val txExtraId: String?, val externalTxId: String, val externalTxUrl: String, val txExtraIdName: String?, - val txExtraId: String?, ) : ExpressTransactionModel() } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index cc91593e45..4d0d6904c4 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -12,4 +12,13 @@ sealed class Warning { data class MinAmountWarning(val dustValue: BigDecimal) : Warning() data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() + + sealed class Cardano : Warning() { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Cardano() + + data object InsufficientBalanceToTransferCoin : Cardano() + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Cardano() + } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 6ba68ddd31..bd29c42170 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -26,4 +26,6 @@ sealed class SwapTransactionState { data object UnknownError : SwapTransactionState() data class ExpressError(val dataError: DataError) : SwapTransactionState() + + data object DemoMode : SwapTransactionState() } \ No newline at end of file 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 62a0ba68e6..a91c30859e 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 @@ -2,15 +2,18 @@ package com.tangem.feature.swap.domain import arrow.core.Either import arrow.core.getOrElse -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.common.extensions.hexToBytes +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.common.extensions.minimalAmount +import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -20,7 +23,10 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -34,13 +40,12 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.isNullOrZero -import com.tangem.utils.toFiatString import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -59,6 +64,7 @@ internal class SwapInteractorImpl @Inject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val walletManagersFacade: WalletManagersFacade, private val sendTransactionUseCase: SendTransactionUseCase, + private val createTransactionUseCase: CreateTransactionUseCase, private val quotesRepository: QuotesRepository, private val dispatcher: CoroutineDispatcherProvider, private val swapTransactionRepository: SwapTransactionRepository, @@ -66,6 +72,8 @@ internal class SwapInteractorImpl @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, + private val demoConfig: DemoConfig, + private val transactionRepository: TransactionRepository, ) : SwapInteractor { private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -343,6 +351,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = TxFeeState.Empty, + transactionFee = null, includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ) } @@ -372,6 +381,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, + minAdaValue: BigDecimal?, ): List { val fromToken = fromTokenStatus.currency val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() @@ -379,6 +389,14 @@ internal class SwapInteractorImpl @Inject constructor( manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken, feeState) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) manageReduceAmountWarning(warnings, fromTokenStatus, amount) + manageTransactionValidationWarnings( + warnings = warnings, + fromToken = fromToken, + amount = amount, + feeState = feeState, + userWalletId = userWalletId, + minAdaValue = minAdaValue, + ) return warnings } @@ -418,23 +436,35 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, ) { + if (BlockchainUtils.isCardano(fromTokenStatus.currency.network.id.value)) return + val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } - val dust = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) - val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO - if (dust != null && - !balance.isNullOrZero() && - amount.value < balance - ) { - val change = balance - (amount.value + fee) - val isChangeLowerThanDust = change < dust && change != BigDecimal.ZERO - val isShowWarning = amount.value + fee < dust || isChangeLowerThanDust - if (isShowWarning) { - warnings.add(Warning.MinAmountWarning(dust)) + + val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) ?: return + + val change = when (fromTokenStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO + balance - (fee + amount.value) } + is CryptoCurrency.Token -> { + val nativeTokenBalance = userWalletManager.getNativeTokenBalance( + fromTokenStatus.currency.network.id.value, + fromTokenStatus.currency.network.derivationPath.value, + ) + + nativeTokenBalance?.value?.minus(fee) ?: BigDecimal.ZERO + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + + if (amount.value < dustValue || isChangeLowerThanDust) { + warnings.add(Warning.MinAmountWarning(dustValue)) } } @@ -449,6 +479,85 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun manageTransactionValidationWarnings( + warnings: MutableList, + fromToken: CryptoCurrency, + amount: SwapAmount, + feeState: TxFeeState, + userWalletId: UserWalletId, + minAdaValue: BigDecimal?, + ) { + val fee = Fee.Common( + amount = Amount( + value = when (feeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue + is TxFeeState.SingleFeeState -> feeState.fee.feeValue + }, + blockchain = Blockchain.fromId(fromToken.network.id.value), + ), + ) + + transactionRepository.validateTransaction( + amount = amount.value.convertToAmount(fromToken), + fee = fee, + memo = null, + txExtras = null, + destination = getTokenAddress(fromToken), + userWalletId = userWalletId, + network = fromToken.network, + ) + .fold( + onFailure = { + addCardanoTransactionValidationError( + warnings = warnings, + error = it as? BlockchainSdkError.Cardano ?: return@fold, + fromToken = fromToken, + userWalletId = userWalletId, + ) + }, + onSuccess = { + minAdaValue?.let { + warnings.add( + Warning.Cardano.MinAdaValueCharged( + tokenName = fromToken.name, + minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), + ), + ) + } + }, + ) + } + + private suspend fun addCardanoTransactionValidationError( + warnings: MutableList, + error: BlockchainSdkError.Cardano, + fromToken: CryptoCurrency, + userWalletId: UserWalletId, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (fromToken) { + is CryptoCurrency.Coin -> Warning.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) + } + } + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromToken.network) ?: return + + Warning.MinAmountWarning(dustValue) + } + } + .let(warnings::add) // add warning to the list + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -458,6 +567,9 @@ internal class SwapInteractorImpl @Inject constructor( includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, ): SwapTransactionState { + val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError + if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.DemoMode + return when (swapProvider.type) { ExchangeProviderType.CEX -> { val amountDecimal = toBigDecimalOrNull(amountToSwap) @@ -480,10 +592,11 @@ internal class SwapInteractorImpl @Inject constructor( onSwapDex( networkId = currencyToSend.currency.network.backendId, swapData = requireNotNull(swapData), - currencyToSend = currencyToSend.currency, - currencyToGet = currencyToGet.currency, + currencyToSendStatus = currencyToSend, + currencyToGetStatus = currencyToGet, amountToSwap = amountToSwap, fee = fee, + userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } } @@ -530,55 +643,85 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun onSwapDex( networkId: String, swapData: SwapDataModel, - currencyToSend: CryptoCurrency, - currencyToGet: CryptoCurrency, + currencyToSendStatus: CryptoCurrencyStatus, + currencyToGetStatus: CryptoCurrencyStatus, amountToSwap: String, fee: TxFee, + userWalletId: UserWalletId, ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } - val amount = SwapAmount(amountDecimal, currencyToSend.decimals) - val derivationPath = currencyToSend.network.derivationPath.value - val result = transactionManager.sendTransaction( - txData = SwapTxData( - networkId = networkId, - amountToSend = amountDecimal, - currencyToSend = swapCurrencyConverter.convert(currencyToSend), - feeAmount = fee.feeValue, - gasLimit = fee.gasLimit, - destinationAddress = swapData.transaction.txTo, - dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData, + val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) + val derivationPath = currencyToSendStatus.currency.network.derivationPath.value + val dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData + val txData = createTransactionUseCase( + amount = amount.value.convertToAmount(currencyToSendStatus.currency), + fee = getFeeForTransaction( + fee = fee, + blockchain = Blockchain.fromId(currencyToSendStatus.currency.network.id.value), ), + memo = null, + destination = swapData.transaction.txTo, + userWalletId = userWalletId, + network = currencyToSendStatus.currency.network, + txExtras = createDexTxExtras(fee.gasLimit, dataToSign), + hash = dataToSign, isSwap = true, - derivationPath = derivationPath, - analyticsData = AnalyticsData( - feeType = fee.feeType.getNameForAnalytics(), - tokenSymbol = currencyToSend.symbol, - ), + ).getOrElse { + Timber.e(it) + return SwapTransactionState.UnknownError + } + + val result = sendTransactionUseCase( + txData = txData, + userWallet = requireNotNull(getSelectedWallet()), + network = currencyToSendStatus.currency.network, ) - return when (result) { - is SendTxResult.Success -> { - storeLastCryptoCurrencyId(currencyToGet) + return result.fold( + ifRight = { txHash -> + repository.exchangeSent( + txId = swapData.transaction.txId, + fromNetwork = currencyToSendStatus.currency.network.backendId, + fromAddress = currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + payInAddress = txData.destinationAddress, + txHash = txHash, + payInExtraId = swapData.transaction.txExtraId, + ) + storeLastCryptoCurrencyId(currencyToGetStatus.currency) SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, - currencyToSend.symbol, + currencyToSendStatus.currency.symbol, ), fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( swapData.toTokenAmount, - currencyToGet.symbol, + currencyToGetStatus.currency.symbol, ), toAmountValue = swapData.toTokenAmount.value, txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) - } - SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTxResult.NetworkError -> SwapTransactionState.NetworkError - is SendTxResult.UnknownError -> SwapTransactionState.UnknownError - } + }, + ifLeft = { + when (it) { + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode + else -> SwapTransactionState.UnknownError + } + }, + ) + } + + private fun createDexTxExtras(gasLimit: Int, data: String): TransactionExtras { + // for now we support only Ethereum like DEX + // need to be extended if we support other blockchains in DEX + return EthereumTransactionExtras( + gasLimit = gasLimit.toBigInteger(), + data = data.removePrefix(HEX_PREFIX).hexToBytes(), + ) } @Suppress("LongMethod") @@ -594,13 +737,14 @@ internal class SwapInteractorImpl @Inject constructor( fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, toContractAddress = currencyToGet.currency.getContractAddress(), + fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = currencyToGet.currency.decimals, providerId = swapProvider.providerId, rateType = RateType.FLOAT, - toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", + toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(), refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null ).getOrElse { return SwapTransactionState.ExpressError(it) } @@ -615,7 +759,7 @@ internal class SwapInteractorImpl @Inject constructor( if (txExtras == null && exchangeDataCex.txExtraId != null) { return SwapTransactionState.UnknownError } - val txData = walletManagersFacade.createTransaction( + val txData = createTransactionUseCase( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction( fee = txFee, @@ -625,18 +769,17 @@ internal class SwapInteractorImpl @Inject constructor( destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, - )?.copy( - extras = txExtras, - ) + ).getOrElse { + Timber.e(it) + return SwapTransactionState.UnknownError + }.copy(extras = txExtras) val result = sendTransactionUseCase( - requireNotNull(txData), + txData = txData, userWallet = requireNotNull(getSelectedWallet()), network = currencyToSend.currency.network, ) - val txCexModel = exchangeData.transaction as? ExpressTransactionModel.CEX - val derivationPath = currencyToSend.currency.network.derivationPath.value return result.fold( ifLeft = { @@ -645,12 +788,21 @@ internal class SwapInteractorImpl @Inject constructor( is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode else -> SwapTransactionState.UnknownError } }, - ifRight = { + ifRight = { txHash -> + repository.exchangeSent( + txId = exchangeDataCex.txId, + fromNetwork = currencyToSend.currency.network.backendId, + fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), + payInAddress = txData.destinationAddress, + txHash = txHash, + payInExtraId = exchangeDataCex.txExtraId, + ) val timestamp = System.currentTimeMillis() - val txExternalUrl = txCexModel?.externalTxUrl + val txExternalUrl = exchangeDataCex.externalTxUrl storeSwapTransaction( currencyToSend = currencyToSend, currencyToGet = currencyToGet, @@ -658,8 +810,8 @@ internal class SwapInteractorImpl @Inject constructor( swapProvider = swapProvider, swapDataModel = exchangeData, timestamp = timestamp, - txExternalUrl = txExternalUrl.orEmpty(), - txExternalId = txCexModel?.externalTxId.orEmpty(), + txExternalUrl = txExternalUrl, + txExternalId = exchangeDataCex.externalTxId, ) storeLastCryptoCurrencyId(currencyToGet.currency) SwapTransactionState.TxSent( @@ -809,10 +961,10 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun createEmptyAmountState(): SwapState { val appCurrency = getSelectedAppCurrencyUseCase.unwrap() return SwapState.EmptyAmountState( - zeroAmountEquivalent = BigDecimal.ZERO.toFiatString( - rateValue = BigDecimal.ONE, - fiatCurrencyName = appCurrency.symbol, - formatWithSpaces = true, + zeroAmountEquivalent = BigDecimalFormatter.formatFiatAmount( + fiatAmount = BigDecimal.ZERO, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, ), ) } @@ -834,8 +986,16 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { + val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> + getUnhandledFee( + amount = amount.value, + userWalletId = userWalletId, + cryptoCurrency = fromToken, + ) + } + val txFee = if (provider.type == ExchangeProviderType.CEX) { - getFeeForCex(amount, fromTokenStatus) + getFeeForCex(txFeeResult, fromTokenStatus) } else { TxFeeState.Empty } @@ -874,11 +1034,13 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = txFee, + transactionFee = txFeeResult?.getOrNull(), includeFeeInAmount = includeFeeInAmount, ) } } + @Suppress("LongMethod") private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, quoteDataModel: Either, @@ -889,6 +1051,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, txFee: TxFeeState, + transactionFee: TransactionFee?, includeFeeInAmount: IncludeFeeInAmount, ): SwapState { return quoteDataModel.fold( @@ -902,7 +1065,12 @@ internal class SwapInteractorImpl @Inject constructor( swapData = null, txFeeState = txFee, ).copy( - warnings = manageWarnings(fromToken, amount, txFee), + warnings = manageWarnings( + fromTokenStatus = fromToken, + amount = amount, + feeState = txFee, + minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, + ), ) when (exchangeProviderType) { @@ -1043,7 +1211,11 @@ internal class SwapInteractorImpl @Inject constructor( val rates = getQuotes(feeCurrencyId) return rates[feeCurrencyId]?.fiatRate?.let { rate -> fees.map { fee -> - fee.toFiatString(rate, appCurrency.symbol, true) + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate.multiply(fee), + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) } }.orEmpty() } @@ -1064,13 +1236,14 @@ internal class SwapInteractorImpl @Inject constructor( fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), + fromAddress = fromToken.value.networkAddress?.defaultAddress?.value.orEmpty(), toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, - toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "", + toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), ).fold( ifRight = { swapData -> val feeData = transactionManager.getFee( @@ -1105,7 +1278,14 @@ internal class SwapInteractorImpl @Inject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, - warnings = manageWarnings(fromToken, amount, txFeeState), + warnings = manageWarnings( + fromTokenStatus = fromToken, + amount = amount, + feeState = txFeeState, + minAdaValue = (feeData as? ProxyFees.SingleFee)?.let { + (it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue + }, + ), preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, @@ -1171,23 +1351,26 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun getFeeForCex(amount: SwapAmount, fromToken: CryptoCurrencyStatus): TxFeeState { - getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> - val txFeeResult = estimateFeeUseCase( - amount = amount.value, - userWalletId = userWalletId, - cryptoCurrency = fromToken.currency, - ).firstOrNull() - return txFeeResult?.fold( - ifLeft = { - TxFeeState.Empty - }, - ifRight = { txFee -> - txFee.toTxFeeState(fromToken.currency) - }, - ) ?: TxFeeState.Empty - } - return TxFeeState.Empty + private suspend fun getFeeForCex( + txFeeResult: Either?, + fromToken: CryptoCurrencyStatus, + ): TxFeeState { + return txFeeResult?.fold( + ifLeft = { TxFeeState.Empty }, + ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) }, + ) ?: TxFeeState.Empty + } + + private suspend fun getUnhandledFee( + amount: BigDecimal, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Either? { + return estimateFeeUseCase( + amount = amount, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).firstOrNull() } @Suppress("LongParameterList", "LongMethod") @@ -1489,6 +1672,7 @@ internal class SwapInteractorImpl @Inject constructor( return amountToSwap.replace(",", ".").toBigDecimalOrNull() } + @Suppress("LongMethod", "CyclomaticComplexMethod") private suspend fun getFeeState( fee: BigDecimal?, spendAmount: SwapAmount, @@ -1559,6 +1743,20 @@ internal class SwapInteractorImpl @Inject constructor( ) } } + is FeePaidCurrency.FeeResource -> { + val network = repository.getNativeTokenForNetwork(networkId).network + val isFeeResourceEnough = currencyChecksRepository.checkIfFeeResourceEnough( + amount = spendAmount.value, + userWalletId = userWalletId, + network = network, + ) + + if (isFeeResourceEnough) { + SwapFeeState.Enough + } else { + SwapFeeState.NotEnough() + } + } } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index afb2f253a4..1a6a6e8b22 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -11,9 +11,10 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -40,6 +41,7 @@ class SwapDomainModule { @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, + @SwapScope createTransactionUseCase: CreateTransactionUseCase, quotesRepository: QuotesRepository, swapTransactionRepository: SwapTransactionRepository, appCurrencyRepository: AppCurrencyRepository, @@ -48,6 +50,7 @@ class SwapDomainModule { coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, currenciesRepository: CurrenciesRepository, + transactionRepository: TransactionRepository, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -57,6 +60,7 @@ class SwapDomainModule { getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, sendTransactionUseCase = sendTransactionUseCase, + createTransactionUseCase = createTransactionUseCase, quotesRepository = quotesRepository, walletManagersFacade = walletManagersFacade, dispatcher = coroutineDispatcherProvider, @@ -65,6 +69,8 @@ class SwapDomainModule { currencyChecksRepository = currencyChecksRepository, currenciesRepository = currenciesRepository, initialToCurrencyResolver = initialToCurrencyResolver, + demoConfig = DemoConfig(), + transactionRepository = transactionRepository, ) } @@ -79,8 +85,8 @@ class SwapDomainModule { @SwapScope @Provides @Singleton - fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletSyncUseCase { + return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) } @Provides @@ -106,13 +112,11 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetCardTokensListUseCase { return GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - dispatchers = dispatchers, ) } @@ -122,6 +126,15 @@ class SwapDomainModule { return IsDemoCardUseCase(config = DemoConfig()) } + @SwapScope + @Provides + @Singleton + fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { + return CreateTransactionUseCase( + transactionRepository = transactionRepository, + ) + } + @SwapScope @Provides @Singleton diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index c78375b5e4..643cf8f201 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.settings) @@ -56,6 +57,9 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) + /** Libs */ + implementation(projects.libs.crypto) + /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.compose.accompanist.webView) @@ -67,5 +71,4 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) - } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index e5db542bba..cd22de2a3b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -20,7 +20,6 @@ data class SwapStateHolder( val alert: SwapWarning.GenericWarning? = null, val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, - val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01 val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, @@ -111,6 +110,7 @@ sealed interface SwapWarning { val type: GenericWarningType = GenericWarningType.OTHER, val onClick: () -> Unit, ) : SwapWarning + data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning @@ -118,6 +118,16 @@ sealed interface SwapWarning { data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning + + sealed interface Cardano : SwapWarning { + val notificationConfig: NotificationConfig + + data class MinAdaValueCharged(override val notificationConfig: NotificationConfig) : Cardano + + data class InsufficientBalanceToTransferCoin(override val notificationConfig: NotificationConfig) : Cardano + + data class InsufficientBalanceToTransferToken(override val notificationConfig: NotificationConfig) : Cardano + } } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 87e4d70044..6bbee12717 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -16,7 +16,6 @@ data class UiActions( val onMaxAmountSelected: () -> Unit, val onReduceAmount: (SwapAmount) -> Unit, val onLeaveExistentialDeposit: (SwapAmount) -> Unit, - val onReduceAmountIgnoreClick: () -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 3b16f0df21..6409755135 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.swap.router.CustomTabsManager import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -25,7 +25,7 @@ import javax.inject.Inject class SwapFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var reduxNavController: ReduxNavController diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 35f1fb8fe4..8b1ac6f243 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -23,6 +23,7 @@ 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.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.FeeItemState @@ -168,7 +169,7 @@ private fun ChooseFeeBottomSheetContent_Preview() { ), ).toImmutableList() Column { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { ChooseFeeBottomSheetContent( ChooseFeeBottomSheetConfig( selectedFee = FeeType.NORMAL, @@ -183,7 +184,7 @@ private fun ChooseFeeBottomSheetContent_Preview() { SpacerH24() - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { ChooseFeeBottomSheetContent( ChooseFeeBottomSheetConfig( selectedFee = FeeType.NORMAL, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 4015137fcd..960b9d3373 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState @@ -128,7 +129,7 @@ private fun ChooseProviderBottomSheet_Preview() { alertText = stringReference("Unavailable"), ), ) - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { ChooseProviderBottomSheetContent( ChooseProviderBottomSheetConfig( selectedProviderId = "1", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index f379ae3511..e4c3b974ca 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.components.rows.SimpleActionRow 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.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.FeeItemState @@ -63,13 +64,13 @@ private fun FeeItemPreview() { onClick = {}, ) Column { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { FeeItem(state = state) } SpacerH24() - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { FeeItem(state = state) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 723d81e622..d94153492f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -11,8 +12,6 @@ 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.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -27,6 +26,9 @@ 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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA +import com.tangem.core.ui.utils.GrayscaleColorFilter import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState @@ -36,11 +38,6 @@ import com.tangem.feature.swap.models.states.ProviderState * https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4 */ -private const val GRAY_SCALE_SATURATION = 0f -private const val GRAY_SCALE_ALPHA = 0.4f -private val GrayscaleColorFilter: ColorFilter - get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) - @Composable fun ProviderItemBlock(state: ProviderState, modifier: Modifier = Modifier) { if (state !is ProviderState.Empty) { @@ -390,25 +387,12 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ProviderItemPreview_Light( +private fun ProviderItemPreview( @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, ) { - TangemTheme { - ProviderItem( - modifier = Modifier.background(TangemTheme.colors.background.action), - state = state.first, - isSelected = state.second, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun ProviderItemPreview_Dark( - @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ProviderItem( modifier = Modifier.background(TangemTheme.colors.background.action), state = state.first, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 188ec65b0b..c902406819 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -88,7 +88,6 @@ internal class StateBuilder( onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, - reduceAmountIgnore = false, priceImpact = PriceImpact.Empty(), ) } @@ -217,7 +216,6 @@ internal class StateBuilder( val warnings = getWarningsForSuccessState( quoteModel = quoteModel, fromToken = fromToken, - ignoreAmountReduce = uiStateHolder.reduceAmountIgnore, selectedFeeType = selectedFeeType, ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) @@ -317,11 +315,10 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, - ignoreAmountReduce: Boolean, selectedFeeType: FeeType, ): List { val warnings = mutableListOf() - maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) + maybeAddDomainWarnings(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) @@ -357,11 +354,7 @@ internal class StateBuilder( } } - private fun maybeAddDomainWarnings( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ignoreAmountReduce: Boolean, - ) { + private fun maybeAddDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { quoteModel.warnings.forEach { when (it) { is Warning.ExistentialDepositWarning -> { @@ -385,24 +378,30 @@ internal class StateBuilder( ) } is Warning.ReduceAmountWarning -> { - if (!ignoreAmountReduce) { - warnings.add( - SwapWarning.ReduceAmount( - notificationConfig = createReduceAmountNotificationConfig( - currencyName = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, - amount = it.tezosFeeThreshold.toPlainString(), - onConfirmClick = { - val fromAmount = quoteModel.fromTokenInfo.tokenAmount - val patchedAmount = fromAmount.copy( - value = fromAmount.value - it.tezosFeeThreshold, - ) - actions.onReduceAmount(patchedAmount) - }, - onDismissClick = actions.onReduceAmountIgnoreClick, - ), + warnings.add( + SwapWarning.ReduceAmount( + notificationConfig = createReduceAmountNotificationConfig( + currencyName = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, + amount = it.tezosFeeThreshold.toPlainString(), + onConfirmClick = { + val fromAmount = quoteModel.fromTokenInfo.tokenAmount + val patchedAmount = fromAmount.copy( + value = fromAmount.value - it.tezosFeeThreshold, + ) + actions.onReduceAmount(patchedAmount) + }, ), - ) - } + ), + ) + } + Warning.Cardano.InsufficientBalanceToTransferCoin -> { + warnings.add(createInsufficientBalanceToTransferCoin()) + } + is Warning.Cardano.InsufficientBalanceToTransferToken -> { + warnings.add(createInsufficientBalanceToTransferToken(tokenName = it.tokenName)) + } + is Warning.Cardano.MinAdaValueCharged -> { + warnings.add(createMinAdaValueCharged(minAdaValue = it.minAdaValue, tokenName = it.tokenName)) } } } @@ -569,9 +568,13 @@ internal class StateBuilder( val preparedSwapConfigState = quoteModel.preparedSwapConfigState // check has has outgoing transaction if (preparedSwapConfigState.hasOutgoingTransaction) return false + // check has MinAmountWarning warning val hasCriticalWarning = quoteModel.warnings.any { - it is Warning.MinAmountWarning || it is Warning.ExistentialDepositWarning + it is Warning.MinAmountWarning || + it is Warning.Cardano.InsufficientBalanceToTransferCoin || + it is Warning.Cardano.InsufficientBalanceToTransferToken || + it is Warning.ExistentialDepositWarning } if (hasCriticalWarning) return false @@ -1046,6 +1049,18 @@ internal class StateBuilder( ) } + fun createDemoModeAlert(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder { + return uiState.copy( + alert = SwapWarning.GenericWarning( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + onClick = onAlertClick, + type = GenericWarningType.OTHER, + ), + changeCardsButtonState = ChangeCardsButtonState.ENABLED, + ) + } + private fun getProviderErrorMessage(dataError: DataError): TextReference? { return when (dataError) { is DataError.SwapsAreUnavailableNowError -> resourceReference( @@ -1409,17 +1424,14 @@ internal class StateBuilder( currencyName: String, amount: String, onConfirmClick: () -> Unit, - onDismissClick: () -> Unit, ): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.send_notification_high_fee_title), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), iconResId = R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( - primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), - onPrimaryClick = onConfirmClick, - secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore), - onSecondaryClick = onDismissClick, + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), + onClick = onConfirmClick, ), ) } @@ -1463,6 +1475,42 @@ internal class StateBuilder( iconResId = R.drawable.img_attention_20, ) } + + private fun createMinAdaValueCharged(minAdaValue: String, tokenName: String): SwapWarning { + return SwapWarning.Cardano.MinAdaValueCharged( + NotificationConfig( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + iconResId = R.drawable.img_attention_20, + ), + ) + } + + private fun createInsufficientBalanceToTransferCoin(): SwapWarning { + return SwapWarning.Cardano.InsufficientBalanceToTransferCoin( + NotificationConfig( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + } + + private fun createInsufficientBalanceToTransferToken(tokenName: String): SwapWarning { + return SwapWarning.Cardano.InsufficientBalanceToTransferToken( + NotificationConfig( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + } // end region private fun getShortAddressValue(fullAddress: String): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt index b915b4c0a5..8ac175588a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,6 +18,7 @@ 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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.ApprovePermissionButton import com.tangem.feature.swap.models.ApproveType import com.tangem.feature.swap.models.CancelPermissionButton @@ -291,17 +293,10 @@ private fun getTitleForApproveType(approveType: ApproveType): String = when (app // region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementBottomSheet_InLightTheme() { - TangemTheme(isDark = false) { - SwapPermissionBottomSheetContent(content = previewData) - } -} - -@Preview -@Composable -private fun Preview_AgreementBottomSheet_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AgreementBottomSheet() { + TangemThemePreview { SwapPermissionBottomSheetContent(content = previewData) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index e68875f3ee..7be20ace87 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -30,6 +31,7 @@ import com.tangem.core.ui.extensions.getActiveIconResByCoinId 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.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* @@ -389,7 +391,8 @@ private fun SwapWarnings(warnings: List) { }, ) } - else -> {} + is SwapWarning.Cardano -> Notification(config = warning.notificationConfig) + SwapWarning.InsufficientFunds -> Unit } SpacerH8() } @@ -497,7 +500,6 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, - reduceAmountIgnore = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), @@ -512,10 +514,11 @@ private val state = SwapStateHolder( ), ) -@Preview +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun SwapScreenContentPreview() { - TangemTheme(isDark = false) { + TangemThemePreview { SwapScreenContent(state = state, modifier = Modifier) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 930039d697..081b492357 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -27,6 +27,7 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration 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.feature.swap.models.* import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList @@ -303,7 +304,7 @@ private val title = TokenToSelectState.Title( @Preview @Composable private fun TokenScreenPreview() { - TangemTheme(isDark = false) { + TangemThemePreview { SwapSelectTokenScreen( state = SwapSelectTokenStateHolder( availableTokens = listOf(title, token, token, token).toImmutableList(), @@ -320,7 +321,7 @@ private fun TokenScreenPreview() { @Preview @Composable private fun EmptyTokensListPreview() { - TangemTheme(isDark = false) { + TangemThemePreview { EmptyTokensList() } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index a562a22435..11ade5db09 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -18,6 +19,7 @@ import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R @@ -167,19 +169,11 @@ private val state = SwapSuccessStateHolder( ) @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Success_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_Success() { + TangemThemePreview { SwapSuccessScreen(state) {} } } - -@Preview(showBackground = true) -@Composable -private fun Preview_Success_InDarkTheme() { - TangemTheme(isDark = true) { - SwapSuccessScreen(state) {} - } -} - // endregion preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index e54128fe4d..7ae3edc6a2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -38,6 +38,7 @@ import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType @@ -61,7 +62,7 @@ fun TransactionCard( Box( modifier = modifier .background( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), + shape = RoundedCornerShape(TangemTheme.dimens.radius16), color = TangemTheme.colors.background.primary, ) .fillMaxSize(), @@ -177,9 +178,9 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie .fillMaxWidth() .padding( bottom = TangemTheme.dimens.spacing8, - top = TangemTheme.dimens.spacing12, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing14, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, ), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -191,7 +192,6 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie maxLines = 1, style = MaterialTheme.typography.subtitle2, modifier = Modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size24) .align(Alignment.CenterVertically), ) SpacerW16() @@ -202,7 +202,6 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie color = TangemTheme.colors.text.tertiary, style = MaterialTheme.typography.body2, modifier = Modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size24) .align(Alignment.CenterVertically), ) } @@ -228,8 +227,8 @@ private fun Content( Row( modifier = Modifier .padding( - start = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing16, ), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.Top, @@ -279,7 +278,7 @@ private fun Content( } } - SpacerH8() + SpacerH4() if (amountEquivalent != null) { if (type is TransactionCardType.ReadOnly) { @@ -499,7 +498,7 @@ private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: In @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCard_InLightTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreview() } } @@ -507,7 +506,7 @@ private fun Preview_TransactionCard_InLightTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithPriceImpact_InLightTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithPriceImpact() } } @@ -515,7 +514,7 @@ private fun Preview_TransactionCardWithPriceImpact_InLightTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithoutPriceImpact_InLightTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithoutPriceImpact() } } @@ -523,7 +522,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InLightTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCard_InDarkTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreview() } } @@ -531,7 +530,7 @@ private fun Preview_TransactionCard_InDarkTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithPriceImpact_InDarkTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithPriceImpact() } } @@ -539,7 +538,7 @@ private fun Preview_TransactionCardWithPriceImpact_InDarkTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithoutPriceImpact() } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt index eb2df9fe19..00a27d8a22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -14,6 +15,7 @@ import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig @Composable @@ -55,21 +57,10 @@ private fun WebViewBottomSheetContent(content: WebViewBottomSheetConfig) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_WebViewBottomSheetContent_InLightTheme() { - TangemTheme(isDark = false) { - WebViewBottomSheetContent( - content = WebViewBottomSheetConfig( - url = "https://tangem.com/en/", - ), - ) - } -} - -@Preview -@Composable -private fun Preview_WebViewBottomSheetContent_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_WebViewBottomSheetContent() { + TangemThemePreview { WebViewBottomSheetContent( content = WebViewBottomSheetConfig( url = "https://tangem.com/en/", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d85a174ead..ee09df73c4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -540,6 +540,12 @@ internal class SwapViewModel @Inject constructor( is SwapTransactionState.UserCancelled -> { startLoadingQuotesFromLastState() } + is SwapTransactionState.DemoMode -> { + startLoadingQuotesFromLastState() + uiState = stateBuilder.createDemoModeAlert(uiState) { + uiState = stateBuilder.clearAlert(uiState) + } + } else -> { startLoadingQuotesFromLastState() uiState = stateBuilder.createErrorTransaction(uiState, it) { @@ -886,12 +892,6 @@ internal class SwapViewModel @Inject constructor( onMaxAmountSelected = ::onMaxAmountClicked, onReduceAmount = ::onReduceAmountClicked, onLeaveExistentialDeposit = ::onLeaveExistentialDepositClicked, - onReduceAmountIgnoreClick = { - uiState = uiState.copy( - reduceAmountIgnore = true, - warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount }, - ) - }, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt new file mode 100644 index 0000000000..a64be25542 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt @@ -0,0 +1,9 @@ +package com.tangem.features.tester.api + +/** + * Interface for app restarter + */ +interface AppRestarter { + + fun restart() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt new file mode 100644 index 0000000000..22d2d4d3a9 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.tester + +import android.app.Activity + +/** + * Wraps the main activity class to avoid type erasure issues during injection. + * + * @property clazz activity class + */ +class ActivityClassWrapper( + val clazz: Class, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt new file mode 100644 index 0000000000..34cd927e71 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.tester.apprestarter + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.tangem.feature.tester.ActivityClassWrapper +import com.tangem.features.tester.api.AppRestarter + +/** + * Entity that kills the process and restarts the main activity + * @property context Activity context + */ +internal class DefaultAppRestarter( + private val context: Context, + private val activityClassWrapper: ActivityClassWrapper, +) : AppRestarter { + + override fun restart() { + if (context !is Activity) return + + context.finish() + context.startActivity( + Intent(context, activityClassWrapper.clazz).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP }, + ) + Runtime.getRuntime().exit(0) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt new file mode 100644 index 0000000000..4891a60f12 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.tester.di + +import android.content.Context +import com.tangem.feature.tester.ActivityClassWrapper +import com.tangem.feature.tester.apprestarter.DefaultAppRestarter +import com.tangem.features.tester.api.AppRestarter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object RestarterModule { + + @Provides + @ActivityScoped + fun provideAppRestarter( + @ActivityContext context: Context, + activityClassWrapper: ActivityClassWrapper, + ): AppRestarter { + return DefaultAppRestarter(context, activityClassWrapper) + } +} \ 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 13c5799f38..ef7ab21998 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 @@ -6,10 +6,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen @@ -18,6 +18,7 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.features.tester.api.AppRestarter import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -27,12 +28,15 @@ import javax.inject.Inject internal class TesterActivity : ComposeActivity() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies /** Router for inner feature navigation */ @Inject lateinit var testerRouter: TesterRouter + @Inject + lateinit var appRestarter: AppRestarter + private val innerTesterRouter: InnerTesterRouter get() = requireNotNull(testerRouter as? InnerTesterRouter) { "TesterRouter must be InnerTesterRouter for tester feature" @@ -66,7 +70,7 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.FEATURE_TOGGLES.name) { val viewModel = hiltViewModel().apply { - setupNavigation(innerTesterRouter) + setupInteractions(innerTesterRouter, appRestarter) } FeatureTogglesScreen(state = viewModel.uiState) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt index fa65d935e4..c814bf750b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt @@ -3,15 +3,16 @@ package com.tangem.feature.tester.presentation.actions import com.tangem.domain.apptheme.model.AppThemeMode internal data class TesterActionsContentState( - val onBackClick: () -> Unit, val hideAllCurrenciesConfig: HideAllCurrenciesConfig, val toggleAppThemeConfig: ToggleAppThemeConfig, + val onBackClick: () -> Unit, + val onApplyChangesClick: () -> Unit, ) internal sealed class HideAllCurrenciesConfig { data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesConfig() - object Progress : HideAllCurrenciesConfig() + data object Progress : HideAllCurrenciesConfig() } internal data class ToggleAppThemeConfig( diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index f8d9df0a78..1cc0845d8c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.actions +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -12,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R @@ -76,26 +78,20 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) { ) { TesterActionsScreen( state = TesterActionsContentState( - onBackClick = {}, hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable {}, toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT) {}, + onBackClick = {}, + onApplyChangesClick = {}, ), ) } } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TesterActionsScreenPreview_Light() { - TangemTheme { - TesterActionsScreenSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun TesterActionsScreenPreview_Dark() { - TangemTheme(isDark = true) { +private fun TesterActionsScreenPreview() { + TangemThemePreview { TesterActionsScreenSample() } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index b919f37132..514edb448d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -31,9 +31,10 @@ internal class TesterActionsViewModel @Inject constructor( private val initialState: TesterActionsContentState get() = TesterActionsContentState( - onBackClick = { /* no-op */ }, hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this::hideAllCurrencies), toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT, this::toggleAppTheme), + onBackClick = { /* no-op */ }, + onApplyChangesClick = { /* no-op */ }, ) init { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt index 4c55258e0d..736e01b6c6 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -8,9 +8,11 @@ import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatur * @property featureToggles feature toggles list * @property onBackClick the lambda to be invoked when back button is pressed * @property onToggleValueChange the lambda to be invoked when switch button is pressed + * @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed */ internal data class FeatureTogglesContentState( val featureToggles: List, - val onBackClick: () -> Unit, val onToggleValueChange: (String, Boolean) -> Unit, + val onBackClick: () -> Unit, + val onApplyChangesClick: () -> Unit, ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 7ce0c9ff6d..b43d13d2f3 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.featuretoggles.ui +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,8 +19,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState @@ -49,6 +52,14 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) }, ) } + item { + PrimaryButton( + text = stringResource(id = R.string.apply_changes), + onClick = state.onApplyChangesClick, + modifier = Modifier.fillMaxWidth() + .padding(TangemTheme.dimens.spacing16), + ) + } } } @@ -81,26 +92,10 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewFeatureTogglesScreen_InLightTheme() { - TangemTheme(isDark = false) { - FeatureTogglesScreen( - state = FeatureTogglesContentState( - featureToggles = listOf( - TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), - TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), - ), - onToggleValueChange = { _, _ -> }, - onBackClick = {}, - ), - ) - } -} - -@Preview -@Composable -private fun PreviewFeatureTogglesScreen_InDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewFeatureTogglesScreen() { + TangemThemePreview { FeatureTogglesScreen( state = FeatureTogglesContentState( featureToggles = listOf( @@ -109,6 +104,7 @@ private fun PreviewFeatureTogglesScreen_InDarkTheme() { ), onToggleValueChange = { _, _ -> }, onBackClick = {}, + onApplyChangesClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 037946a145..d3459ac31f 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -10,6 +10,7 @@ import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.features.tester.api.AppRestarter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -38,16 +39,20 @@ internal class FeatureTogglesViewModel @Inject constructor( "Feature toggle manager must be mutable (debug build type)" } - /** Setup navigation state property by router [router] */ - fun setupNavigation(router: InnerTesterRouter) { - uiState = uiState.copy(onBackClick = router::back) + /** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */ + fun setupInteractions(router: InnerTesterRouter, appRestarter: AppRestarter) { + uiState = uiState.copy( + onBackClick = router::back, + onApplyChangesClick = appRestarter::restart, + ) } private fun initState(): FeatureTogglesContentState { return FeatureTogglesContentState( featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), - onBackClick = {}, onToggleValueChange = ::onToggleValueChange, + onBackClick = {}, + onApplyChangesClick = {}, ) } 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 4e5d7f056f..39d9c5b95c 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.menu.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState @@ -64,23 +66,10 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewTesterMenuScreen_InLightTheme() { - TangemTheme(isDark = false) { - TesterMenuScreen( - state = TesterMenuContentState( - onBackClick = {}, - onFeatureTogglesClick = {}, - onTesterActionsClick = {}, - ), - ) - } -} - -@Preview -@Composable -private fun PreviewTesterMenuScreen_InDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewTesterMenuScreen() { + TangemThemePreview { TesterMenuScreen( state = TesterMenuContentState( onBackClick = {}, diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 9a5a5846b6..ffc3114361 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Tester menu Feature toggles + Apply changes Stand toggles Tester actions Hide all currencies diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt index d64095a963..1fc4007823 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt @@ -1,5 +1,6 @@ package com.tangem.features.tokendetails.featuretoggles interface TokenDetailsFeatureToggles { + fun isGenerateXPubEnabled(): Boolean } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 6db0120517..bff55fb9ad 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) @@ -73,6 +74,8 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.transaction) + implementation(projects.domain.staking) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt index 1df3fcf7be..96d2dbbb72 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt @@ -6,5 +6,6 @@ import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggle internal class DefaultTokenDetailsFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokenDetailsFeatureToggles { + override fun isGenerateXPubEnabled() = featureTogglesManager.isFeatureEnabled(name = "GENERATE_XPUB_ENABLED") } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index bea2fb5a9f..bd8ea330aa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -4,10 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel @@ -19,7 +20,7 @@ import javax.inject.Inject internal class TokenDetailsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter @@ -41,6 +42,6 @@ internal class TokenDetailsFragment : ComposeFragment() { setSystemBarsColor(systemBarsColor) } - TokenDetailsScreen(state = viewModel.uiState) + TokenDetailsScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 69d878168f..470cc0ac26 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -35,7 +35,7 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( name = "Stellar (XLM) with long name test", - iconState = TokenInfoBlockState.IconState.CoinIcon( + iconState = IconState.CoinIcon( url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", fallbackResId = R.drawable.img_stellar_22, isGrayscale = false, @@ -44,7 +44,7 @@ internal object TokenDetailsPreviewData { ) val tokenInfoBlockStateWithLongName = TokenInfoBlockState( name = "Tether (USDT) with long name test", - iconState = TokenInfoBlockState.IconState.TokenIcon( + iconState = IconState.TokenIcon( url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", fallbackTint = Color.Cyan, fallbackBackground = Color.Blue, @@ -59,7 +59,7 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockStateWithLongNameNoStandard = TokenInfoBlockState( name = "Tether (USDT) with long name test", - iconState = TokenInfoBlockState.IconState.TokenIcon( + iconState = IconState.TokenIcon( url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", fallbackTint = Color.Cyan, fallbackBackground = Color.Blue, @@ -74,7 +74,7 @@ internal object TokenDetailsPreviewData { val tokenInfoBlockState = TokenInfoBlockState( name = "Tether USDT", - iconState = TokenInfoBlockState.IconState.CustomTokenIcon( + iconState = IconState.CustomTokenIcon( tint = Color.Green, background = Color.Magenta, isGrayscale = true, @@ -86,11 +86,18 @@ internal object TokenDetailsPreviewData { ), ) + val iconState = IconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackTint = Color.Cyan, + fallbackBackground = Color.Blue, + isGrayscale = false, + ) + private val actionButtons = persistentListOf( - TokenDetailsActionButton.Buy(enabled = true, onClick = {}), - TokenDetailsActionButton.Send(enabled = true, onClick = {}), - TokenDetailsActionButton.Receive(onClick = {}), - TokenDetailsActionButton.Swap(enabled = true, onClick = {}), + TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), + TokenDetailsActionButton.Send(dimContent = false, onClick = {}), + TokenDetailsActionButton.Receive(onClick = {}, onLongClick = null), + TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons) @@ -103,6 +110,8 @@ internal object TokenDetailsPreviewData { private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT") + private val stakingLoading = StakingBlockState.Loading(iconState = iconState) + private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( isRefreshing = false, onRefresh = {}, @@ -237,6 +246,7 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, + stakingBlockState = stakingLoading, notifications = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -250,6 +260,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, + isStakingAvailable = false, event = consumedEvent(), ) @@ -267,6 +278,12 @@ internal object TokenDetailsPreviewData { type = PriceChangeType.UP, ), ), + stakingBlockState = StakingBlockState.Content( + interestRate = "7.38", + periodInDays = 4, + tokenSymbol = "XLM", + iconState = iconState, + ), notifications = persistentListOf(), txHistoryState = TxHistoryState.NotSupported( onExploreClick = {}, @@ -279,6 +296,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, + isStakingAvailable = true, event = consumedEvent(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index cf4d420f32..a69442cd51 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -37,12 +37,13 @@ internal class TokenDetailsNotificationsAnalyticsSender( ) is TokenDetailsNotification.NetworksUnreachable, is TokenDetailsNotification.ExistentialDeposit, - is TokenDetailsNotification.HasPendingTransactions, is TokenDetailsNotification.NetworksNoAccount, is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, is TokenDetailsNotification.NetworkShutdown, + is TokenDetailsNotification.HederaAssociateWarning, + is TokenDetailsNotification.KoinosMana, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt new file mode 100644 index 0000000000..a3e44c1324 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/IconState.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +internal sealed class IconState { + + abstract val isGrayscale: Boolean + + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + ) : IconState() + + data class TokenIcon( + val url: String?, + val fallbackTint: Color, + val fallbackBackground: Color, + override val isGrayscale: Boolean, + ) : IconState() + + data class CustomTokenIcon( + val tint: Color, + val background: Color, + override val isGrayscale: Boolean, + ) : IconState() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt new file mode 100644 index 0000000000..f1668235b6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed interface StakingBlockState { + + val iconState: IconState + + data class Error(override val iconState: IconState) : StakingBlockState + + data class Loading(override val iconState: IconState) : StakingBlockState + + data class Content( + override val iconState: IconState, + val interestRate: String, + val periodInDays: Int, + val tokenSymbol: String, + ) : StakingBlockState +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index f9f5dfcabd..26f36f81ad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -17,6 +17,7 @@ internal data class TokenDetailsState( val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val stakingBlockState: StakingBlockState, val notifications: ImmutableList, val pendingTxs: PersistentList, val swapTxs: PersistentList, @@ -26,5 +27,6 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, + val isStakingAvailable: Boolean, val event: StateEvent, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index f0ec9e8d43..9e54bb1627 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -2,7 +2,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color internal data class TokenInfoBlockState( val name: String, @@ -11,7 +10,7 @@ internal data class TokenInfoBlockState( ) { @Immutable sealed class Currency { - object Native : Currency() + data object Native : Currency() /** * @param standardName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc. @@ -24,29 +23,4 @@ internal data class TokenInfoBlockState( @DrawableRes val networkIcon: Int, ) : Currency() } - - @Immutable - sealed class IconState { - - abstract val isGrayscale: Boolean - - data class CoinIcon( - val url: String?, - @DrawableRes val fallbackResId: Int, - override val isGrayscale: Boolean, - ) : IconState() - - data class TokenIcon( - val url: String?, - val fallbackTint: Color, - val fallbackBackground: Color, - override val isGrayscale: Boolean, - ) : IconState() - - data class CustomTokenIcon( - val tint: Color, - val background: Color, - override val isGrayscale: Boolean, - ) : IconState() - } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index 63130c7889..c5b6078527 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -11,46 +11,53 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { /** Lambda be invoked when manage button is clicked */ abstract val onClick: () -> Unit + /** Lambda be invoked when manage button is long clicked */ + open val onLongClick: (() -> TextReference?)? = null + /** * Buy * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Buy button is clicked */ - data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Buy(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) /** * Send * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Send button is clicked */ - data class Send(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Send(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) /** * Receive - * * @property onClick lambda be invoked when Receive button is clicked + * @property onLongClick lambda be invoked when Receive button is long clicked */ - data class Receive(override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Receive( + override val onClick: () -> Unit, + override val onLongClick: (() -> TextReference?)?, + ) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, + onLongClick = onLongClick, enabled = true, ), ) @@ -58,30 +65,30 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { /** * Sell * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Sell button is clicked */ - data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Sell(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_sell), iconResId = R.drawable.ic_currency_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) /** * Swap * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Swap button is clicked */ - data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Swap(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt index 02fc3679f3..47949f59c7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -19,7 +19,7 @@ internal data class TokenDetailsDialogConfig( sealed class DialogContentConfig { - abstract val title: TextReference + abstract val title: TextReference? abstract val message: TextReference abstract val confirmButtonConfig: ButtonConfig abstract val cancelButtonConfig: ButtonConfig? @@ -31,13 +31,13 @@ internal data class TokenDetailsDialogConfig( ) data class ConfirmHideConfig( - val currencySymbol: String, + val currencyTitle: String, val onConfirmClick: () -> Unit, val onCancelClick: () -> Unit, ) : DialogContentConfig() { override val title: TextReference = TextReference.Res( id = R.string.token_details_hide_alert_title, - formatArgs = wrappedList(currencySymbol), + formatArgs = wrappedList(currencyTitle), ) override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message) @@ -55,6 +55,7 @@ internal data class TokenDetailsDialogConfig( } data class HasLinkedTokensConfig( + val currencyName: String, val currencySymbol: String, val networkName: String, val onConfirmClick: () -> Unit, @@ -66,7 +67,7 @@ internal data class TokenDetailsDialogConfig( override val message: TextReference = TextReference.Res( id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(currencySymbol, networkName), + formatArgs = wrappedList(currencyName, currencySymbol, networkName), ) override val cancelButtonConfig: ButtonConfig? @@ -77,5 +78,39 @@ internal data class TokenDetailsDialogConfig( onClick = onConfirmClick, ) } + + data class DisabledButtonReasonDialogConfig( + val text: TextReference, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + + override val title = null + + override val message: TextReference = text + + override val cancelButtonConfig = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } + + data class ErrorDialogConfig( + val text: TextReference, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + + override val title = null + + override val message: TextReference = text + + override val cancelButtonConfig = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 874efc22b9..950f24a218 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -2,10 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.componen import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.features.tokendetails.impl.R @@ -66,7 +63,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) - object NetworksUnreachable : Warning( + data object NetworksUnreachable : Warning( title = resourceReference(R.string.warning_network_unreachable_title), subtitle = resourceReference(R.string.warning_network_unreachable_message), ) @@ -117,8 +114,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ), iconResId = currency.networkIconResId, - buttonsState = - NotificationConfig.ButtonsState.SecondaryButtonConfig( + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference( id = R.string.common_buy_currency, formatArgs = wrappedList( @@ -167,21 +163,46 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) - object TopUpWithoutReserve : Informational( + data object TopUpWithoutReserve : Informational( title = resourceReference(id = R.string.warning_no_account_title), subtitle = resourceReference(id = R.string.no_account_send_to_create), ) - class HasPendingTransactions(val coinSymbol: String) : Informational( - title = resourceReference(R.string.warning_send_blocked_pending_transactions_title), - subtitle = resourceReference( - id = R.string.warning_send_blocked_pending_transactions_message, - formatArgs = wrappedList(coinSymbol), - ), - ) - data class NetworkShutdown(private val title: TextReference, private val subtitle: TextReference) : Warning( title = title, subtitle = subtitle, ) + + data class HederaAssociateWarning( + private val currency: CryptoCurrency, + private val fee: String?, + private val feeCurrencySymbol: String?, + private val onAssociateClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.warning_hedera_missing_token_association_title), + subtitle = if (fee != null && feeCurrencySymbol != null) { + resourceReference( + id = R.string.warning_hedera_missing_token_association_message, + formatArgs = wrappedList(fee, feeCurrencySymbol), + ) + } else { + resourceReference(R.string.warning_hedera_missing_token_association_message_brief) + }, + iconResId = currency.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_hedera_missing_token_association_button_title), + onClick = onAssociateClick, + ), + ) + + data class KoinosMana( + val manaBalanceAmount: String, + val maxManaBalanceAmount: String, + ) : Informational( + title = resourceReference(id = R.string.koinos_mana_level_title), + subtitle = resourceReference( + id = R.string.koinos_mana_level_description, + formatArgs = wrappedList(manaBalanceAmount, maxManaBalanceAmount), + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt index 956cdaf6d5..dbf8a99f3c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton @@ -26,19 +27,34 @@ internal class TokenDetailsActionButtonsConverter( .mapNotNull { action -> when (action) { is TokenActionsState.ActionState.Buy -> { - TokenDetailsActionButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick) + TokenDetailsActionButton.Buy( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onBuyClick(action.unavailabilityReason) }, + ) } is TokenActionsState.ActionState.Receive -> { - TokenDetailsActionButton.Receive(onClick = clickIntents::onReceiveClick) + TokenDetailsActionButton.Receive( + onClick = { clickIntents.onReceiveClick(action.unavailabilityReason) }, + onLongClick = clickIntents::onCopyAddress, + ) } is TokenActionsState.ActionState.Sell -> { - TokenDetailsActionButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) + TokenDetailsActionButton.Sell( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onSellClick(action.unavailabilityReason) }, + ) } is TokenActionsState.ActionState.Send -> { - TokenDetailsActionButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) + TokenDetailsActionButton.Send( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onSendClick(action.unavailabilityReason) }, + ) } is TokenActionsState.ActionState.Swap -> { - TokenDetailsActionButton.Swap(enabled = action.enabled, onClick = clickIntents::onSwapClick) + TokenDetailsActionButton.Swap( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onSwapClick(action.unavailabilityReason) }, + ) } else -> { null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt index c428da7cec..cfbfcb7015 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsIconStateConverter.kt @@ -4,39 +4,39 @@ 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.tokens.model.CryptoCurrency -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.utils.converter.Converter -internal class TokenDetailsIconStateConverter : Converter { +internal class TokenDetailsIconStateConverter : Converter { - override fun convert(value: CryptoCurrency): TokenInfoBlockState.IconState { + override fun convert(value: CryptoCurrency): IconState { return when (value) { is CryptoCurrency.Coin -> getIconStateForCoin(value) is CryptoCurrency.Token -> getIconStateForToken(value) } } - private fun getIconStateForCoin(coin: CryptoCurrency.Coin): TokenInfoBlockState.IconState.CoinIcon { - return TokenInfoBlockState.IconState.CoinIcon( + private fun getIconStateForCoin(coin: CryptoCurrency.Coin): IconState.CoinIcon { + return IconState.CoinIcon( url = coin.iconUrl, fallbackResId = coin.networkIconResId, isGrayscale = coin.network.isTestnet, ) } - private fun getIconStateForToken(token: CryptoCurrency.Token): TokenInfoBlockState.IconState { + private fun getIconStateForToken(token: CryptoCurrency.Token): IconState { val isGrayscale = token.network.isTestnet val background = token.tryGetBackgroundForTokenIcon(isGrayscale) val tint = getTintForTokenIcon(background) return if (token.isCustom && token.iconUrl == null) { - TokenInfoBlockState.IconState.CustomTokenIcon( + IconState.CustomTokenIcon( tint = tint, background = background, isGrayscale = isGrayscale, ) } else { - TokenInfoBlockState.IconState.TokenIcon( + IconState.TokenIcon( url = token.iconUrl, isGrayscale = isGrayscale, fallbackTint = tint, 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 5048215ac6..63d735d3ad 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 @@ -84,7 +84,7 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun getMarketPriceState( - status: CryptoCurrencyStatus.Status, + status: CryptoCurrencyStatus.Value, currencySymbol: String, ): MarketPriceBlockState { return when (status) { @@ -106,7 +106,7 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun CryptoCurrencyStatus.Status.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content { + private fun CryptoCurrencyStatus.Value.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content { return MarketPriceBlockState.Content( currencySymbol = currencySymbol, price = formatPrice(status = this, appCurrency = appCurrencyProvider()), @@ -117,11 +117,11 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType { return PriceChangeConverter.fromBigDecimal(status.priceChange) } - private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatPercent( @@ -130,17 +130,17 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatFiatAmount( + return BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = fiatRate, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatFiatAmount( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 23080d5d8e..fd828b47d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -1,10 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* @@ -14,6 +16,8 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber +import java.math.BigDecimal internal class TokenDetailsNotificationConverter( private val clickIntents: TokenDetailsClickIntents, @@ -29,6 +33,13 @@ internal class TokenDetailsNotificationConverter( return newNotifications.toImmutableList() } + fun removeHederaAssociateWarning(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is HederaAssociateWarning } + return newNotifications.toImmutableList() + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { return when (warning) { is CryptoCurrencyWarning.BalanceNotEnoughForFee -> NetworkFeeWithBuyButton( @@ -67,13 +78,14 @@ internal class TokenDetailsNotificationConverter( CryptoCurrencyWarning.SomeNetworksUnreachable -> NetworksUnreachable is CryptoCurrencyWarning.SomeNetworksNoAccount -> NetworksNoAccount( network = warning.amountCurrency.name, - amount = warning.amountToCreateAccount.toString(), + amount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = warning.amountToCreateAccount, + cryptoCurrency = "", + decimals = warning.amountCurrency.decimals, + ), symbol = warning.amountCurrency.symbol, ) is CryptoCurrencyWarning.TopUpWithoutReserve -> TopUpWithoutReserve - is CryptoCurrencyWarning.HasPendingTransactions -> HasPendingTransactions( - coinSymbol = warning.blockchainSymbol, - ) is CryptoCurrencyWarning.SwapPromo -> SwapPromo( startDateTime = warning.startDateTime, endDateTime = warning.endDateTime, @@ -84,9 +96,43 @@ internal class TokenDetailsNotificationConverter( title = resourceReference(R.string.warning_beacon_chain_retirement_title), subtitle = resourceReference(R.string.warning_beacon_chain_retirement_content), ) + + is HederaWarnings.AssociateWarning -> HederaAssociateWarning( + currency = warning.currency, + fee = null, + feeCurrencySymbol = null, + onAssociateClick = clickIntents::onAssociateClick, + ) + is HederaWarnings.AssociateWarningWithFee -> HederaAssociateWarning( + currency = warning.currency, + fee = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = warning.fee, + cryptoCurrency = "", + decimals = warning.feeCurrencyDecimals, + ), + feeCurrencySymbol = warning.feeCurrencySymbol, + onAssociateClick = clickIntents::onAssociateClick, + ) + is CryptoCurrencyWarning.FeeResourceInfo -> KoinosMana( + manaBalanceAmount = formatMana(warning.amount), + maxManaBalanceAmount = warning.maxAmount?.let { + formatMana(it) + } ?: run { + Timber.e("FeeResource maxAmount cannot be null in Koinos. Check KoinosWalletManager") + "" + }, + ) } } + private fun formatMana(amount: BigDecimal): String { + return BigDecimalFormatter.formatCryptoAmountShorted( + cryptoAmount = amount, + cryptoCurrency = "", + decimals = Blockchain.Koinos.decimals(), + ) + } + // workaround for networks that users have misunderstanding private fun CryptoCurrency.shouldMergeFeeNetworkName(): Boolean { return Blockchain.fromNetworkId(this.network.backendId) == Blockchain.Arbitrum 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 163a1516ae..2c29e74ce3 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 @@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.tokendetails.presentation.tokendetails.state.* @@ -16,6 +17,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -25,11 +27,13 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, + private val stakingAvailabilityProvider: Provider, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } override fun convert(value: CryptoCurrency): TokenDetailsState { + val iconState = iconStateConverter.convert(value) return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, @@ -37,7 +41,7 @@ internal class TokenDetailsSkeletonStateConverter( ), tokenInfoBlockState = TokenInfoBlockState( name = value.name, - iconState = iconStateConverter.convert(value), + iconState = iconState, currency = when (value) { is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( @@ -49,6 +53,7 @@ internal class TokenDetailsSkeletonStateConverter( ), tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()), marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), + stakingBlockState = StakingBlockState.Loading(iconState = iconState), notifications = persistentListOf(), pendingTxs = persistentListOf(), swapTxs = persistentListOf(), @@ -62,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, + isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available, event = consumedEvent(), ) } @@ -88,11 +94,11 @@ internal class TokenDetailsSkeletonStateConverter( private fun createButtons(): ImmutableList { return persistentListOf( - TokenDetailsActionButton.Buy(enabled = false, onClick = {}), - TokenDetailsActionButton.Send(enabled = false, onClick = {}), - TokenDetailsActionButton.Receive(onClick = {}), - TokenDetailsActionButton.Sell(enabled = false, onClick = {}), - TokenDetailsActionButton.Swap(enabled = false, onClick = {}), + TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), + TokenDetailsActionButton.Send(dimContent = false, onClick = {}), + TokenDetailsActionButton.Receive(onClick = {}, onLongClick = null), + TokenDetailsActionButton.Sell(dimContent = false, onClick = {}), + TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) } 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 06b0c838c7..c47c971328 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 @@ -12,14 +12,14 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent 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.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver +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.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError @@ -40,10 +40,11 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val stakingAvailabilityProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, symbol: String, @@ -54,6 +55,7 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, featureToggles = featureToggles, + stakingAvailabilityProvider = stakingAvailabilityProvider, ) } @@ -100,6 +102,12 @@ internal class TokenDetailsStateFactory( ) } + private val stakingStateConverter by lazy { + TokenStakingStateConverter( + currentStateProvider = currentStateProvider, + ) + } + fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert(value = screenArgument) } @@ -155,7 +163,7 @@ internal class TokenDetailsStateFactory( isShow = true, onDismissRequest = clickIntents::onDismissDialog, content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig( - currencySymbol = currency.symbol, + currencyTitle = currency.name, onConfirmClick = clickIntents::onHideConfirmed, onCancelClick = clickIntents::onDismissDialog, ), @@ -169,6 +177,7 @@ internal class TokenDetailsStateFactory( isShow = true, onDismissRequest = clickIntents::onDismissDialog, content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig( + currencyName = currency.name, currencySymbol = currency.symbol, networkName = currency.network.name, onConfirmClick = clickIntents::onDismissDialog, @@ -177,6 +186,38 @@ internal class TokenDetailsStateFactory( ) } + fun getStateWithActionButtonErrorDialog(unavailabilityReason: ScenarioUnavailabilityReason): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig( + text = getUnavailabilityReasonText(unavailabilityReason), + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } + + fun getStateWithErrorDialog(text: TextReference): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ErrorDialogConfig( + text = text, + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } + + fun getStateWithStaking(stakingEither: Either): TokenDetailsState { + return currentStateProvider().copy( + stakingBlockState = stakingStateConverter.convert(stakingEither), + ) + } + fun getRefreshingState(): TokenDetailsState { return refreshStateConverter.convert(true) } @@ -246,6 +287,11 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.removeRentInfo(state)) } + fun getStateWithRemovedHederaAssociateNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeHederaAssociateWarning(state)) + } + fun getStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( @@ -321,4 +367,60 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } + + private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { + return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.PendingTransaction -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_send, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + } + } + is ScenarioUnavailabilityReason.EmptyBalance -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_send, + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_sell, + ) + } + } + is ScenarioUnavailabilityReason.BuyUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_buy_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotExchangeable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotSupportedBySellService -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.Unreachable -> { + resourceReference( + id = R.string.token_button_unavailability_generic_description, + ) + } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) + ScenarioUnavailabilityReason.None -> { + throw IllegalArgumentException("The unavailability reason must be other than None") + } + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt new file mode 100644 index 0000000000..371e2e766d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import arrow.core.Either +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter + +internal class TokenStakingStateConverter( + private val currentStateProvider: Provider, +) : Converter, StakingBlockState> { + + override fun convert(value: Either): StakingBlockState { + value.fold( + ifLeft = { + return StakingBlockState.Error( + iconState = currentStateProvider().tokenInfoBlockState.iconState, + ) + }, + ifRight = { + return StakingBlockState.Content( + interestRate = BigDecimalFormatter.formatPercent( + percent = it.interestRate, + useAbsoluteValue = true, + ), + periodInDays = it.periodInDays, + tokenSymbol = it.tokenSymbol, + iconState = currentStateProvider().tokenInfoBlockState.iconState, + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 070d2b0065..d9eb9ec18d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box @@ -39,9 +40,12 @@ import com.tangem.core.ui.event.StateEvent 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.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.* import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar @@ -131,6 +135,14 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } + if (state.isStakingAvailable) { + item( + key = StakingBlockState::class.java, + contentType = StakingBlockState::class.java, + content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) }, + ) + } + swapTransactionsItems( state.swapTxs, itemModifier, @@ -184,21 +196,12 @@ internal fun TokenDetailsEventEffect(snackbarHostState: SnackbarHostState, event // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TokenDetailsScreenPreview_Light( +private fun TokenDetailsScreenPreview( @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, ) { - TangemTheme { - TokenDetailsScreen(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun TokenDetailsScreenPreview_Dark( - @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokenDetailsScreen(state) } } 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 e8fd329b15..3f448ae33a 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -14,6 +15,7 @@ import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips 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 @@ -129,21 +131,12 @@ private fun CryptoBalance( } @Preview(widthDp = 328, heightDp = 152) +@Preview(widthDp = 328, heightDp = 152, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenDetailsBalanceBlock_LightTheme( +private fun Preview_TokenDetailsBalanceBlock( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { - TangemTheme(isDark = false) { - TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) - } -} - -@Preview(widthDp = 328, heightDp = 152) -@Composable -private fun Preview_TokenDetailsBalanceBlock_DarkTheme( - @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt index 174eb35567..ed54c9459b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -25,7 +25,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { onClick = config.content.confirmButtonConfig.onClick, ), onDismissDialog = config.onDismissRequest, - title = config.content.title.resolveReference(), + title = config.content.title?.resolveReference(), dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> DialogButton( title = cancelButtonConfig.text.resolveReference(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index 1ed6603ba3..3ea60b1140 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -18,6 +19,7 @@ import androidx.compose.ui.util.fastForEach 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.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig @@ -96,9 +98,10 @@ private fun AppBarDropdownItem( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() { - TangemTheme(isDark = false) { +private fun Preview_TokenDetailsAppBarDropdownItem() { + TangemThemePreview { AppBarDropdownItem( modifier = Modifier.background(TangemTheme.colors.background.primary), dismissParent = {}, @@ -112,33 +115,10 @@ private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenDetailsAppBarDropdownItem_DarkTheme() { - TangemTheme(isDark = true) { - AppBarDropdownItem( - modifier = Modifier.background(TangemTheme.colors.background.primary), - dismissParent = {}, - item = TokenDetailsAppBarMenuConfig.MenuItem( - title = TextReference.Res(id = R.string.token_details_hide_token), - textColorProvider = { TangemTheme.colors.text.warning }, - onClick = { }, - ), - ) - } -} - -@Preview -@Composable -private fun Preview_TokenDetailsTopAppBar_LightTheme() { - TangemTheme(isDark = false) { - TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) - } -} - -@Preview -@Composable -private fun Preview_TokenDetailsTopAppBar_DarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_TokenDetailsTopAppBar() { + TangemThemePreview { TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt index 8bf7ed9b78..c0b325c9e4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenIcon.kt @@ -21,26 +21,21 @@ import coil.request.ImageRequest import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.features.tokendetails.impl.R import kotlinx.coroutines.launch @Composable -internal fun CurrencyIcon( - icon: TokenInfoBlockState.IconState, - alpha: Float, - colorFilter: ColorFilter?, - modifier: Modifier = Modifier, -) { +internal fun CurrencyIcon(icon: IconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { when (icon) { - is TokenInfoBlockState.IconState.CoinIcon -> CoinIcon( + is IconState.CoinIcon -> CoinIcon( modifier = modifier, url = icon.url, fallbackResId = icon.fallbackResId, alpha = alpha, colorFilter = colorFilter, ) - is TokenInfoBlockState.IconState.TokenIcon -> TokenIcon( + is IconState.TokenIcon -> TokenIcon( modifier = modifier, url = icon.url, alpha = alpha, @@ -54,7 +49,7 @@ internal fun CurrencyIcon( ) }, ) - is TokenInfoBlockState.IconState.CustomTokenIcon -> CustomTokenIcon( + is IconState.CustomTokenIcon -> CustomTokenIcon( modifier = modifier, tint = icon.tint, background = icon.background, 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 3982a8dc21..773dc2868f 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Text @@ -10,22 +11,20 @@ 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.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA +import com.tangem.core.ui.utils.GrayscaleColorFilter +import com.tangem.core.ui.utils.NORMAL_ALPHA import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R -private const val GRAY_SCALE_SATURATION = 0f -private const val GRAY_SCALE_ALPHA = 0.4f -private const val NORMAL_ALPHA = 1f - @Composable internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) { Row( @@ -130,29 +129,19 @@ private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): E } } -private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String) - -private val GrayscaleColorFilter: ColorFilter - get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) +private data class ExtractedTokenNetworkText( + val normalText: String, + val boldText: String, +) @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenInfoBlock_LightTheme( +private fun Preview_TokenInfoBlock( @PreviewParameter(TokenInfoStateProvider::class) state: TokenInfoBlockState, ) { - TangemTheme(isDark = false) { - TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) - } -} - -@Preview -@Composable -private fun Preview_TokenInfoBlock_DarkTheme( - @PreviewParameter(TokenInfoStateProvider::class) - state: TokenInfoBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt new file mode 100644 index 0000000000..6ce576304f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt @@ -0,0 +1,206 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA +import com.tangem.core.ui.utils.GrayscaleColorFilter +import com.tangem.core.ui.utils.NORMAL_ALPHA +import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.features.tokendetails.impl.R + +/** + * Token staking block + * + * @param state component state + * @param modifier modifier + */ +@Composable +internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size72) + .padding(all = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + horizontalAlignment = Alignment.Start, + ) { + Content(state = state) + } +} + +@Composable +private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) { + AnimatedContent( + modifier = modifier.heightIn(min = TangemTheme.dimens.size60), + targetState = state, + contentAlignment = Alignment.CenterStart, + label = "Update the content", + ) { stakingBlockState -> + when (stakingBlockState) { + is StakingBlockState.Content -> { + StakingContent( + stakingBlockState = stakingBlockState, + iconState = stakingBlockState.iconState, + ) + } + is StakingBlockState.Loading -> { + StakingLoading( + iconState = stakingBlockState.iconState, + ) + } + is StakingBlockState.Error -> Row {} // TODO staking + } + } +} + +@Composable +private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) { + Column { + Row { + val (alpha, colorFilter) = remember(iconState.isGrayscale) { + if (iconState.isGrayscale) { + GRAY_SCALE_ALPHA to GrayscaleColorFilter + } else { + NORMAL_ALPHA to null + } + } + CurrencyIcon( + modifier = Modifier + .size(TangemTheme.dimens.size20) + .clip(TangemTheme.shapes.roundedCorners8) + .align(Alignment.CenterVertically), + icon = iconState, + alpha = alpha, + colorFilter = colorFilter, + ) + SpacerW8() + Column { + Text( + text = stringResource( + R.string.token_details_staking_block_title, + stakingBlockState.interestRate, + ), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + + Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) + + Text( + text = stringResource( + R.string.token_details_staking_block_subtitle, + stakingBlockState.tokenSymbol, + stakingBlockState.periodInDays, + ), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) + + Spacer(modifier = Modifier.size(TangemTheme.dimens.size8)) + } + } + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Stake", + onClick = { /* [REDACTED_TODO_COMMENT] */ }, + ) + } +} + +@Composable +private fun StakingLoading(iconState: IconState) { + Column { + Row { + val (alpha, colorFilter) = remember(iconState.isGrayscale) { + if (iconState.isGrayscale) { + GRAY_SCALE_ALPHA to GrayscaleColorFilter + } else { + NORMAL_ALPHA to null + } + } + CurrencyIcon( + modifier = Modifier + .size(TangemTheme.dimens.size20) + .clip(TangemTheme.shapes.roundedCorners8) + .align(Alignment.CenterVertically), + icon = iconState, + alpha = alpha, + colorFilter = colorFilter, + ) + SpacerW8() + Column { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size20), + ) + Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size20), + ) + Spacer(modifier = Modifier.size(TangemTheme.dimens.size8)) + } + } + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Loading", // TODO staking + onClick = { /* [REDACTED_TODO_COMMENT] */ }, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TokenStakingBlock( + @PreviewParameter(StakingBlockStateProvider::class) + state: StakingBlockState, +) { + TangemThemePreview { + TokenStakingBlock(state = state) + } +} + +private class StakingBlockStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + StakingBlockState.Content( + iconState = iconState, + interestRate = "10", + periodInDays = 4, + tokenSymbol = "SOL", + ), + StakingBlockState.Loading(iconState = iconState), + StakingBlockState.Error(iconState = iconState), + ), +) + +private val iconState = IconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackTint = Color.Cyan, + fallbackBackground = Color.Blue, + isGrayscale = false, +) +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt index 5f3527a8eb..7d1c2f14b5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview @Composable internal fun ExchangeProvider( @@ -90,7 +91,7 @@ internal fun ExchangeProvider( @Preview(showBackground = true) @Composable private fun ExchangeProvider_Preview() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { ExchangeProvider( providerName = TextReference.Str("Changelly"), providerType = TextReference.Str("CEX"), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 53e398e71f..1945621d19 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -25,6 +26,7 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.features.tokendetails.impl.R @@ -195,31 +197,12 @@ private fun ExchangeStatusItem( //region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ExchangeStatusItemPreview_Light( +private fun ExchangeStatusItemPreview( @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String, ) { - TangemTheme { - ExchangeStatusItem( - providerName = "ChangeNow", - fromTokenIconState = TokenIconState.Loading, - toTokenIconState = TokenIconState.Loading, - fromAmount = amount, - fromSymbol = "USDT", - toSymbol = "USDT", - onClick = {}, - infoIconRes = null, - infoIconTint = null, - ) - } -} - -@Preview -@Composable -private fun ExchangeStatusItemPreview_Dark( - @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ExchangeStatusItem( providerName = "ChangeNow", fromTokenIconState = TokenIconState.Loading, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index d044397eb9..4bc8e7fb37 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -18,8 +18,8 @@ import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListMode import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter -import com.tangem.utils.Provider import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 712afdeb80..751b8dcd92 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -1,20 +1,24 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason @Suppress("TooManyFunctions") interface TokenDetailsClickIntents { fun onBackClick() - fun onSendClick() + fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) - fun onReceiveClick() + fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) - fun onSellClick() + fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) - fun onSwapClick() + fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) + + fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onDismissDialog() @@ -24,8 +28,6 @@ interface TokenDetailsClickIntents { fun onRefreshSwipe() - fun onBuyClick() - fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) fun onReloadClick() @@ -49,4 +51,8 @@ interface TokenDetailsClickIntents { fun onSwapPromoClick() fun onGenerateExtendedKey() + + fun onCopyAddress(): TextReference? + + fun onAssociateClick() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 02defbde3a..d4e58af373 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -1,8 +1,5 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse @@ -10,9 +7,15 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TxHistoryState +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.haptic.HapticManager import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -22,20 +25,27 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.staking.GetStakingAvailabilityUseCase +import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.error.AssociateAssetError +import com.tangem.domain.transaction.usecase.AssociateAssetUseCase 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.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -58,6 +68,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* @@ -70,7 +81,6 @@ import javax.inject.Inject @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, - private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -88,14 +98,20 @@ internal class TokenDetailsViewModel @Inject constructor( private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, + private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase, + private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val isDemoCardUseCase: IsDemoCardUseCase, + private val associateAssetUseCase: AssociateAssetUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, - featureToggles: TokenDetailsFeatureToggles, + private val hapticManager: HapticManager, + private val clipboardManager: ClipboardManager, + tokenDetailsFeatureToggles: TokenDetailsFeatureToggles, + getUserWalletUseCase: GetUserWalletUseCase, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -107,6 +123,8 @@ internal class TokenDetailsViewModel @Inject constructor( private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] ?: error("This screen can't open without `CryptoCurrency`") + private val userWallet: UserWallet + lateinit var router: InnerTokenDetailsRouter private val marketPriceJobHolder = JobHolder() @@ -120,12 +138,15 @@ internal class TokenDetailsViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() private val stateFactory = TokenDetailsStateFactory( - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + stakingAvailabilityProvider = Provider { + getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value) + }, clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, - featureToggles = featureToggles, + featureToggles = tokenDetailsFeatureToggles, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -139,7 +160,7 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, analyticsEventsHandlerProvider = Provider { analyticsEventsHandler }, - currentStateProvider = Provider { uiState }, + currentStateProvider = Provider { uiState.value }, userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, ) @@ -156,8 +177,8 @@ internal class TokenDetailsViewModel @Inject constructor( TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler) } - var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) - private set + private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) + val uiState: StateFlow = internalUiState init { deepLinksRegistry.registerWithViewModel( @@ -166,6 +187,7 @@ internal class TokenDetailsViewModel @Inject constructor( BuyCurrencyDeepLink(::onBuyCurrencyDeepLink), ), ) + userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found") } private fun onBuyCurrencyDeepLink() { @@ -191,46 +213,47 @@ internal class TokenDetailsViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) + updateStakingInfo() } private fun handleBalanceHiding(owner: LifecycleOwner) { getBalanceHidingSettingsUseCase() .flowWithLifecycle(owner.lifecycle) .onEach { - uiState = stateFactory.getStateWithUpdatedHidden( + internalUiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = it.isBalanceHidden, ) } .launchIn(viewModelScope) } - private suspend fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { return } + private suspend fun updateButtons(currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, ) .conflate() .distinctUntilChanged() - .onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) } - .flowOn(dispatchers.io) + .onEach { + internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) + } + .flowOn(dispatchers.main) .launchIn(viewModelScope) } private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) { - viewModelScope.launch(dispatchers.io) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } + viewModelScope.launch(dispatchers.main) { getCurrencyWarningsUseCase.invoke( userWalletId = userWalletId, currencyStatus = cryptoCurrencyStatus, derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() .onEach { val updatedState = stateFactory.getStateWithNotifications(it) - notificationsAnalyticsSender.send(uiState, updatedState.notifications) - uiState = updatedState + notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) + internalUiState.value = updatedState } .launchIn(viewModelScope) .saveIn(warningsJobHolder) @@ -238,31 +261,30 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - viewModelScope.launch(dispatchers.io) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } + viewModelScope.launch(dispatchers.main) { getCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - uiState = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) + internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status - updateButtons(userWalletId = userWalletId, currencyStatus = status) + updateButtons(currencyStatus = status) updateWarnings(status) } currencyStatusAnalyticsSender.send(maybeCurrencyStatus) } - .flowOn(dispatchers.io) + .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) } } private fun subscribeOnExchangeTransactionsUpdates() { - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { swapTxStatusTaskScheduler.cancelTask() exchangeStatusFactory.invoke() .distinctUntilChanged() @@ -274,8 +296,8 @@ internal class TokenDetailsViewModel @Inject constructor( PeriodicTask( delay = EXCHANGE_STATUS_UPDATE_DELAY, task = { - runCatching(dispatchers.io) { - exchangeStatusFactory.updateSwapTxStatuses(uiState.swapTxs) + runCatching { + exchangeStatusFactory.updateSwapTxStatuses(internalUiState.value.swapTxs) } }, onSuccess = ::updateSwapTx, @@ -283,20 +305,20 @@ internal class TokenDetailsViewModel @Inject constructor( ), ) } - .flowOn(dispatchers.io) + .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(swapTxJobHolder) } } private fun updateSwapTx(swapTxs: PersistentList) { - val config = uiState.bottomSheetConfig + val config = internalUiState.value.bottomSheetConfig val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig val currentTx = swapTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId } if (currentTx?.activeStatus == ExchangeStatus.Finished) { updateNetworkToSwapBalance(currentTx.toCryptoCurrency) } - uiState = uiState.copy( + internalUiState.value = internalUiState.value.copy( swapTxs = swapTxs, bottomSheetConfig = currentTx?.let( stateFactory::updateStateWithExchangeStatusBottomSheet, @@ -319,7 +341,7 @@ internal class TokenDetailsViewModel @Inject constructor( * @param showItemsLoading - show loading items placeholder. */ private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) { - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( userWalletId = userWalletId, currency = cryptoCurrency, @@ -327,9 +349,9 @@ internal class TokenDetailsViewModel @Inject constructor( // if countEither is left, handling error state run inside getLoadingTxHistoryState if (showItemsLoading || txHistoryItemsCountEither.isLeft()) { - uiState = stateFactory.getLoadingTxHistoryState( + internalUiState.value = stateFactory.getLoadingTxHistoryState( itemsCountEither = txHistoryItemsCountEither, - pendingTransactions = uiState.pendingTxs, + pendingTransactions = internalUiState.value.pendingTxs, ) } @@ -340,17 +362,25 @@ internal class TokenDetailsViewModel @Inject constructor( refresh = refresh, ).map { it.cachedIn(viewModelScope) } - uiState = stateFactory.getLoadedTxHistoryState(maybeTxHistory) + internalUiState.value = stateFactory.getLoadedTxHistoryState(maybeTxHistory) + } + } + } + + private fun updateStakingInfo() { + viewModelScope.launch(dispatchers.main) { + val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value) + if (stakingAvailability is StakingAvailability.Available) { + val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) + internalUiState.value = stateFactory.getStateWithStaking(stakingInfo) } } } private fun updateTopBarMenu() { viewModelScope.launch(dispatchers.main) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } - - uiState = stateFactory.getStateWithUpdatedMenu( - cardTypesResolver = wallet.scanResponse.cardTypesResolver, + internalUiState.value = stateFactory.getStateWithUpdatedMenu( + cardTypesResolver = userWallet.scanResponse.cardTypesResolver, isBitcoin = isBitcoin(cryptoCurrency.network.id.value), ) } @@ -372,16 +402,18 @@ internal class TokenDetailsViewModel @Inject constructor( router.popBackStack() } - override fun onBuyClick() { + override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrency.symbol)) + if (handleUnavailabilityReason(unavailabilityReason)) return + showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( TradeCryptoAction.Buy( - userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + userWallet = userWallet, cryptoCurrencyStatus = status, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), @@ -397,13 +429,15 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onReloadClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReload(cryptoCurrency.symbol)) - uiState = stateFactory.getLoadingTxHistoryState() + internalUiState.value = stateFactory.getLoadingTxHistoryState() updateTxHistory(refresh = true, showItemsLoading = true) } - override fun onSendClick() { + override fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrency.symbol)) + if (handleUnavailabilityReason(unavailabilityReason)) return + sendCurrency(status = cryptoCurrencyStatus ?: return) } @@ -416,7 +450,7 @@ internal class TokenDetailsViewModel @Inject constructor( is CryptoCurrency.Coin -> { reduxStateHolder.dispatch( action = TradeCryptoAction.SendCoin( - userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + userWallet = userWallet, coinStatus = status, feeCurrencyStatus = maybeFeeCurrencyStatus, transactionInfo = transactionInfo, @@ -441,13 +475,12 @@ internal class TokenDetailsViewModel @Inject constructor( feeCurrencyStatus: CryptoCurrencyStatus?, transactionInfo: TransactionInfo?, ) { - viewModelScope.launch(dispatchers.io) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } + viewModelScope.launch(dispatchers.main) { val maybeCoinStatus = getNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = tokenCurrency.network.id, derivationPath = tokenCurrency.network.derivationPath, - isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .conflate() .distinctUntilChanged() @@ -455,7 +488,7 @@ internal class TokenDetailsViewModel @Inject constructor( reduxStateHolder.dispatchWithMain( action = TradeCryptoAction.SendToken( - userWallet = wallet, + userWallet = userWallet, tokenCurrency = tokenCurrency, tokenFiatRate = tokenFiatRate, coinFiatRate = maybeCoinStatus?.fold( @@ -469,14 +502,16 @@ internal class TokenDetailsViewModel @Inject constructor( } } - override fun onReceiveClick() { + override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return - viewModelScope.launch(dispatchers.io) { + if (handleUnavailabilityReason(unavailabilityReason)) return + + viewModelScope.launch(dispatchers.main) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened) - uiState = stateFactory.getStateWithReceiveBottomSheet( + internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, networkAddress = networkAddress, sendCopyAnalyticsEvent = { @@ -507,9 +542,11 @@ internal class TokenDetailsViewModel @Inject constructor( } } - override fun onSellClick() { + override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol)) + if (handleUnavailabilityReason(unavailabilityReason)) return + showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse @@ -522,14 +559,16 @@ internal class TokenDetailsViewModel @Inject constructor( } } - override fun onSwapClick() { + override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrency.symbol)) + if (handleUnavailabilityReason(unavailabilityReason)) return + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrency)) } override fun onDismissDialog() { - uiState = stateFactory.getStateWithClosedDialog() + internalUiState.value = stateFactory.getStateWithClosedDialog() } override fun onHideClick() { @@ -537,7 +576,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch { val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency) - uiState = if (hasLinkedTokens) { + internalUiState.value = if (hasLinkedTokens) { stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } else { stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) @@ -561,10 +600,10 @@ internal class TokenDetailsViewModel @Inject constructor( private fun openExplorer() { val currencyStatus = cryptoCurrencyStatus ?: return - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { when (val addresses = currencyStatus.value.networkAddress) { is NetworkAddress.Selectable -> { - uiState = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) + internalUiState.value = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) } is NetworkAddress.Single -> { router.openUrl( @@ -582,14 +621,12 @@ internal class TokenDetailsViewModel @Inject constructor( private fun showErrorIfDemoModeOrElse(action: () -> Unit) { viewModelScope.launch(dispatchers.main) { - val wallet = getUserWalletUseCase(userWalletId = userWalletId).getOrElse { return@launch } - - if (isDemoCardUseCase(cardId = wallet.cardId)) { - uiState = stateFactory.getStateWithClosedBottomSheet() - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, + if (isDemoCardUseCase(cardId = userWallet.cardId)) { + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + internalUiState.value = stateFactory.getStateAndTriggerEvent( + state = internalUiState.value, errorMessage = resourceReference(id = R.string.alert_demo_feature_disabled), - setUiState = { uiState = it }, + setUiState = { internalUiState.value = it }, ) } else { action() @@ -606,7 +643,7 @@ internal class TokenDetailsViewModel @Inject constructor( addressType = AddressType.valueOf(addressModel.type.name), ), ) - uiState = stateFactory.getStateWithClosedBottomSheet() + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() } } @@ -623,9 +660,9 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onRefreshSwipe() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Refreshed(cryptoCurrency.symbol)) - uiState = stateFactory.getRefreshingState() + internalUiState.value = stateFactory.getRefreshingState() - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { listOf( async { fetchCurrencyStatusUseCase( @@ -637,32 +674,32 @@ internal class TokenDetailsViewModel @Inject constructor( async { updateTxHistory( refresh = true, - showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, + showItemsLoading = internalUiState.value.txHistoryState !is TxHistoryState.Content, ) subscribeOnExchangeTransactionsUpdates() }, ).awaitAll() - uiState = stateFactory.getRefreshedState() + internalUiState.value = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) } override fun onDismissBottomSheet() { - if (uiState.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { + if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { viewModelScope.launch(dispatchers.main) { - uiState = exchangeStatusFactory.removeTransactionOnBottomSheetClosed() + internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed() } } - uiState = stateFactory.getStateWithClosedBottomSheet() + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() } override fun onCloseRentInfoNotification() { - uiState = stateFactory.getStateWithRemovedRentNotification() + internalUiState.value = stateFactory.getStateWithRemovedRentNotification() } override fun onSwapTransactionClick(txId: String) { - val swapTxState = uiState.swapTxs.first { it.txId == txId } + val swapTxState = internalUiState.value.swapTxs.first { it.txId == txId } analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTxStatusOpened(cryptoCurrency.symbol)) - uiState = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState) + internalUiState.value = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState) } override fun onGoToProviderClick(url: String) { @@ -681,7 +718,61 @@ internal class TokenDetailsViewModel @Inject constructor( shouldShowSwapPromoTokenUseCase.neverToShow() analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) } - onSwapClick() + onSwapClick(ScenarioUnavailabilityReason.None) + } + + override fun onCopyAddress(): TextReference? { + val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return null + val addresses = networkAddress.availableAddresses.mapToAddressModels(cryptoCurrency).toImmutableList() + val defaultAddress = addresses.firstOrNull()?.value ?: return null + + hapticManager.vibrateMeduim() + clipboardManager.setText(text = defaultAddress) + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + return resourceReference(R.string.wallet_notification_address_copied) + } + + override fun onAssociateClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.Associate( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + viewModelScope.launch(dispatchers.io) { + associateAssetUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifLeft = { e -> + when (e) { + is AssociateAssetError.NotEnoughBalance -> { + internalUiState.value = stateFactory.getStateWithErrorDialog( + resourceReference( + id = R.string.warning_hedera_token_association_not_enough_hbar_message, + formatArgs = wrappedList(e.feeCurrency.symbol), + ), + ) + } + is AssociateAssetError.DataError -> { + internalUiState.value = stateFactory.getStateWithErrorDialog( + stringReference(e.message.orEmpty()), + ) + Timber.e(e.message) + } + } + }, + ifRight = { internalUiState.value = stateFactory.getStateWithRemovedHederaAssociateNotification() }, + ) + } + } + + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { + if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false + + internalUiState.value = stateFactory.getStateWithActionButtonErrorDialog(unavailabilityReason) + + return true } private companion object { diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 6b46d5c5dc..dad4976a7d 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) @@ -86,4 +87,5 @@ dependencies { implementation(projects.features.send.api) implementation(projects.features.tester.api) implementation(projects.features.manageTokens.api) + implementation(projects.features.details.api) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt new file mode 100644 index 0000000000..a358685961 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles +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 FeatureTogglesModule { + + @Provides + @Singleton + fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletFeatureToggles { + return WalletFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt new file mode 100644 index 0000000000..e0881eead6 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.wallet.featuretoggle + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class WalletFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) { + + val isTokenListLceFlowEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("TOKEN_LIST_LCE_ENABLED") +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 74df363767..5a2c923015 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter @@ -21,7 +21,7 @@ import javax.inject.Inject internal class WalletFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject internal lateinit var manageTokensUi: ManageTokensUi diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt index c5af5a2cf6..77f6e6160b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.common.component +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -13,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder @@ -64,7 +66,7 @@ private fun InternalNetworkGroupItem( Row( modifier = Modifier .background(TangemTheme.colors.background.primary) - .padding(horizontal = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing12) .fillMaxWidth() .heightIn(min = TangemTheme.dimens.size40), verticalAlignment = Alignment.CenterVertically, @@ -92,17 +94,10 @@ private fun NetworkGroupItemSample(isDraggable: Boolean) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun NetworkGroupItemPreview_Light(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { - TangemTheme { - NetworkGroupItemSample(isDraggable) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun NetworkGroupItemPreview_Dark(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { - TangemTheme(isDark = true) { +private fun NetworkGroupItemPreview(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { + TangemThemePreview { NetworkGroupItemSample(isDraggable) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 0b20c2a43a..eb3f107e08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.* import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -127,7 +128,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val layoutWidth = constraints.maxWidth val horizontalPadding = with(density) { dimens.size12.roundToPx() } - val verticalPadding = with(density) { dimens.size16.roundToPx() } + val verticalPadding = with(density) { dimens.size15.roundToPx() } val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() @@ -213,8 +214,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier val layoutHeight = calculateLayoutHeight( state = state, minLayoutHeight = with(density) { dimens.size68.roundToPx() }, - layoutPadding = horizontalPadding, - betweenRowsPadding = with(density) { dimens.size2.roundToPx() }, + layoutPadding = verticalPadding, title = title, fiatAmount = fiatAmount, cryptoAmount = cryptoAmount, @@ -359,7 +359,6 @@ private fun calculateLayoutHeight( state: TokenItemState, minLayoutHeight: Int, layoutPadding: Int, - betweenRowsPadding: Int, title: Placeable, fiatAmount: Placeable?, cryptoAmount: Placeable?, @@ -373,9 +372,8 @@ private fun calculateLayoutHeight( is TokenItemState.Loading, is TokenItemState.Locked, -> { - firstColumnHeight = 2 * layoutPadding + title.height + betweenRowsPadding + (cryptoAmount?.height ?: 0) - secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + betweenRowsPadding + - (priceChange?.height ?: 0) + firstColumnHeight = 2 * layoutPadding + title.height + (cryptoAmount?.height ?: 0) + secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + (priceChange?.height ?: 0) } is TokenItemState.Draggable, is TokenItemState.NoAddress, @@ -392,7 +390,7 @@ private fun calculateLayoutHeight( @Preview(widthDp = 360) @Composable private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TokenItem(state = state, isBalanceHidden = false) } } 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 new file mode 100644 index 0000000000..509a1b3085 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -0,0 +1,166 @@ +package com.tangem.feature.wallet.presentation.common.preview + +import androidx.compose.runtime.mutableStateOf +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +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.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.managetokens.navigation.ExpandableState +import kotlinx.collections.immutable.persistentListOf + +internal object WalletScreenPreviewData { + private val tokenItemState = TokenItemState.Content( + id = "1", + iconState = TokenIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"), + cryptoPriceState = TokenItemState.CryptoPriceState.Content( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ) + + private val contentTokensState = WalletTokensListState.ContentState.Content( + items = persistentListOf( + WalletTokensListState.TokensListItemState.NetworkGroupTitle( + id = 1, + name = stringReference("Bitcoin"), + ), + WalletTokensListState.TokensListItemState.Token(state = tokenItemState), + WalletTokensListState.TokensListItemState.NetworkGroupTitle( + id = 2, + name = stringReference("Ethereum"), + ), + WalletTokensListState.TokensListItemState.Token( + state = tokenItemState.copy( + id = "2", + titleState = TokenItemState.TitleState.Content(text = "Ethereum"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "1,856660295 ETH"), + cryptoPriceState = TokenItemState.CryptoPriceState.Content( + price = "1 799,41 \$", + priceChangePercent = "5,16 %", + type = PriceChangeType.UP, + ), + ), + ), + WalletTokensListState.TokensListItemState.Token( + state = TokenItemState.Unreachable( + id = "3", + iconState = TokenIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = "Polygon"), + onItemClick = {}, + onItemLongClick = {}, + ), + ), + WalletTokensListState.TokensListItemState.Token( + state = tokenItemState.copy( + id = "4", + titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "6 200 220,00 SHIB"), + cryptoPriceState = TokenItemState.CryptoPriceState.Content( + price = "0.01 \$", + priceChangePercent = "1,34 %", + type = PriceChangeType.DOWN, + ), + ), + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val noteLockedCard by lazy { + WalletCardState.LockedContent( + id = UserWalletId(stringValue = "1"), + title = "Note", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("Locked"), + ), + imageResId = R.drawable.ill_note_btc_120_106, + onRenameClick = { _ -> }, + onDeleteClick = {}, + ) + } + private val miltiUnreachableCard by lazy { + WalletCardState.Content( + id = UserWalletId(stringValue = "2"), + title = "Wallet 1", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("Seed phrase"), + ), + imageResId = R.drawable.ill_wallet2_cards3_120_106, + cardCount = 3, + balance = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + onRenameClick = { _ -> }, + onDeleteClick = {}, + ) + } + private val multiWalletState by lazy { + WalletState.MultiCurrency.Content( + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + walletCardState = miltiUnreachableCard, + warnings = persistentListOf( + WalletNotification.Warning.SomeNetworksUnreachable, + ), + bottomSheetConfig = null, + tokensListState = contentTokensState, + manageTokensButtonConfig = ManageTokensButtonConfig(onClick = {}), + ) + } + + private val buyButton = WalletManageButton.Buy(enabled = false, dimContent = true, onClick = {}) + private val sendButton = WalletManageButton.Send(enabled = false, dimContent = true, onClick = {}) + private val receiveButton = WalletManageButton.Receive( + enabled = false, + dimContent = true, + onClick = {}, + onLongClick = null, + ) + + private val singleWalletLockedState = WalletState.SingleCurrency.Locked( + walletCardState = noteLockedCard, + buttons = persistentListOf( + buyButton, + sendButton, + receiveButton, + ), + bottomSheetConfig = null, + onUnlockNotificationClick = {}, + onExploreClick = {}, + ) + + internal val walletScreenState = WalletScreenState( + onBackClick = {}, + manageTokensExpandableState = mutableStateOf(ExpandableState.COLLAPSED), + topBarConfig = topBarConfig, + selectedWalletIndex = 0, + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState, + ), + onWalletChange = {}, + event = consumedEvent(), + isHidingMode = false, + manageTokenRedesignToggle = false, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 294fe204cd..4efcf827cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background @@ -34,6 +35,7 @@ import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem @@ -373,21 +375,12 @@ private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun OrganizeTokensScreenPreview_Light( +private fun OrganizeTokensScreenPreview( @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState, ) { - TangemTheme { - OrganizeTokensScreen(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun OrganizeTokensScreenPreview_Dark( - @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { OrganizeTokensScreen(state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5342e0ece5..2540a508f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -7,12 +7,15 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.utils.getOrElse 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.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -39,6 +42,7 @@ internal class OrganizeTokensViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, + private val walletFeatureToggles: WalletFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents { @@ -65,7 +69,7 @@ internal class OrganizeTokensViewModel @Inject constructor( UserWalletId(userWalletIdValue) } - private var tokenList: TokenList? = null + private var cachedTokenList: TokenList? = null val uiState: StateFlow = stateHolder.stateFlow @@ -89,7 +93,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onSortClick() { - val list = tokenList ?: return + val list = cachedTokenList ?: return if (list.sortedBy == TokenList.SortType.BALANCE) return analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) @@ -99,14 +103,14 @@ internal class OrganizeTokensViewModel @Inject constructor( ifLeft = stateHolder::updateStateWithError, ifRight = { stateHolder.updateStateAfterTokenListSorting(it) - tokenList = it + cachedTokenList = it }, ) } } override fun onGroupClick() { - val list = tokenList ?: return + val list = cachedTokenList ?: return analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) @@ -115,7 +119,7 @@ internal class OrganizeTokensViewModel @Inject constructor( ifLeft = stateHolder::updateStateWithError, ifRight = { stateHolder.updateStateAfterTokenListSorting(it) - tokenList = it + cachedTokenList = it }, ) } @@ -138,7 +142,7 @@ internal class OrganizeTokensViewModel @Inject constructor( val result = applyTokenListSortingUseCase( userWalletId = userWalletId, - sortedTokensIds = resolver.resolve(listState, tokenList), + sortedTokensIds = resolver.resolve(listState, cachedTokenList), isGroupedByNetwork = isGroupedByNetwork, isSortedByBalance = isSortedByBalance, ) @@ -161,16 +165,41 @@ internal class OrganizeTokensViewModel @Inject constructor( private fun bootstrapTokenList() { viewModelScope.launch(dispatchers.default) { - val maybeTokenList = getTokenListUseCase(userWalletId) - .first { it.getOrNull()?.totalFiatBalance !is TokenList.FiatBalance.Loading } + val tokenList = getTokenList() ?: return@launch - maybeTokenList.fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateWithTokenList(it) - tokenList = it - }, - ) + stateHolder.updateStateWithTokenList(tokenList) + cachedTokenList = tokenList + } + } + + private suspend fun getTokenList(): TokenList? { + return if (walletFeatureToggles.isTokenListLceFlowEnabled) { + val tokenList = getTokenListUseCase.launchLce(userWalletId) + .transform { maybeTokenList -> + val tokenList = maybeTokenList.getOrElse( + ifLoading = { return@transform }, + ifError = { error -> + stateHolder.updateStateWithError(error) + + return@transform + }, + ) + + emit(tokenList) + } + + tokenList.firstOrNull() + } else { + val maybeTokenList = getTokenListUseCase.launch(userWalletId) + .first { maybeTokenList -> + maybeTokenList.getOrNull()?.totalFiatBalance !is TotalFiatBalance.Loading + } + + maybeTokenList.getOrElse { error -> + stateHolder.updateStateWithError(error) + + null + } } } @@ -189,7 +218,7 @@ internal class OrganizeTokensViewModel @Inject constructor( if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { - tokenList = tokenList?.disableSortingByBalance() + cachedTokenList = cachedTokenList?.disableSortingByBalance() stateHolder.disableSortingByBalance() } } 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 c0f4910bfe..6276cdb2e9 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 @@ -28,6 +28,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.managetokens.navigation.ExpandableState import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -116,8 +117,15 @@ internal class DefaultWalletRouter( navController.navigate(WalletRoute.OrganizeTokens.createRoute(userWalletId)) } - override fun openDetailsScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.Details)) + override fun openDetailsScreen(selectedWalletId: UserWalletId) { + reduxNavController.navigate( + action = NavigationAction.NavigateTo( + screen = AppScreen.Details, + bundle = bundleOf( + DetailsEntryPoint.USER_WALLET_ID_KEY to selectedWalletId.stringValue, + ), + ), + ) } override fun openOnboardingScreen() { 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 a8b311b067..bf8aba542f 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 @@ -35,7 +35,7 @@ internal interface InnerWalletRouter : WalletRouter { fun openOrganizeTokensScreen(userWalletId: UserWalletId) /** Open details screen */ - fun openDetailsScreen() + fun openDetailsScreen(selectedWalletId: UserWalletId) /** Open onboarding screen */ fun openOnboardingScreen() 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 655a49a92b..5d9840556b 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 @@ -2,15 +2,16 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.extensions.isZero import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase import com.tangem.domain.analytics.model.WalletBalanceState -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen @@ -33,9 +34,9 @@ internal class TokenListAnalyticsSender @Inject constructor( private val mutex = Mutex() suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) { - if (screenLifecycleProvider.isBackground) return + if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return - if (tokenList.totalFiatBalance is TokenList.FiatBalance.Loading) return + if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) return val currenciesStatuses = getCurrenciesStatuses(tokenList) @@ -52,7 +53,7 @@ internal class TokenListAnalyticsSender @Inject constructor( } private fun sendBalanceLoadedEventIfNeeded( - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, currenciesStatuses: List, ) { createCardBalanceState(fiatBalance, currenciesStatuses)?.let { balanceState -> @@ -61,13 +62,13 @@ internal class TokenListAnalyticsSender @Inject constructor( } private fun createCardBalanceState( - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, currenciesStatuses: List, ): AnalyticsParam.CardBalanceState? { return when (fiatBalance) { - is TokenList.FiatBalance.Failed -> getCardBalanceState(currenciesStatuses) - is TokenList.FiatBalance.Loaded -> getCardBalanceState(fiatBalance) - is TokenList.FiatBalance.Loading -> null + is TotalFiatBalance.Failed -> getCardBalanceState(currenciesStatuses) + is TotalFiatBalance.Loaded -> getCardBalanceState(fiatBalance) + is TotalFiatBalance.Loading -> null } } @@ -125,7 +126,7 @@ internal class TokenListAnalyticsSender @Inject constructor( } } - private fun getCardBalanceState(fiatBalance: TokenList.FiatBalance.Loaded): AnalyticsParam.CardBalanceState { + private fun getCardBalanceState(fiatBalance: TotalFiatBalance.Loaded): AnalyticsParam.CardBalanceState { return if (fiatBalance.amount > BigDecimal.ZERO) { AnalyticsParam.CardBalanceState.Full } else { @@ -135,7 +136,7 @@ internal class TokenListAnalyticsSender @Inject constructor( private suspend fun sendToppedUpEventIfNeeded( userWallet: UserWallet, - fiatBalance: TokenList.FiatBalance, + fiatBalance: TotalFiatBalance, currenciesStatuses: List, ) { val balanceState = getWalletBalanceState(fiatBalance) ?: return @@ -157,17 +158,17 @@ internal class TokenListAnalyticsSender @Inject constructor( } } - private fun getWalletBalanceState(fiatBalance: TokenList.FiatBalance): WalletBalanceState? { + private fun getWalletBalanceState(fiatBalance: TotalFiatBalance): WalletBalanceState? { return when (fiatBalance) { - is TokenList.FiatBalance.Failed -> WalletBalanceState.Error - is TokenList.FiatBalance.Loaded -> { + is TotalFiatBalance.Failed -> WalletBalanceState.Error + is TotalFiatBalance.Loaded -> { if (fiatBalance.amount > BigDecimal.ZERO) { WalletBalanceState.ToppedUp } else { WalletBalanceState.Empty } } - is TokenList.FiatBalance.Loading -> null + is TotalFiatBalance.Loading -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 16ba03cb43..59d4331387 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -18,7 +18,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( ) { fun send(displayedUiState: WalletState?, newWarnings: List) { - if (screenLifecycleProvider.isBackground) return + if (screenLifecycleProvider.isBackgroundState.value) return if (newWarnings.isEmpty()) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return 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 daaebb07e1..a2adf6b292 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 @@ -47,7 +47,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) } return combine( - flow = getTokenListUseCase(userWallet.walletId).conflate(), + flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = shouldShowTravalaPromoWalletUseCase().conflate(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt index 1a4a34780d..e528262365 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt @@ -6,7 +6,8 @@ import arrow.core.raise.ensure import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.SaveWalletUseCase import javax.inject.Inject @@ -14,6 +15,7 @@ import javax.inject.Inject internal class ScanCardToUnlockWalletClickHandler @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, ) { private var scanFailsCounter = 0 @@ -31,7 +33,7 @@ internal class ScanCardToUnlockWalletClickHandler @Inject constructor( scanFailsCounter = 0 // If card's public key is null then user wallet will be null - val scannedWallet = UserWalletBuilder(scanResponse = result.data).build() + val scannedWallet = UserWalletBuilder(result.data, generateWalletNameUseCase).build() ensure(walletId == scannedWallet?.walletId) { ScanCardToUnlockWalletError.WrongCardIsScanned diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 3613a6439a..722f7de8f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -12,6 +12,7 @@ import com.tangem.feature.wallet.impl.R * [REDACTED_AUTHOR] */ +// TODO: make flexible to integrate cobrands ([REDACTED_JIRA]) internal object WalletImageResolver { private const val WALLET_WITHOUT_BACKUP_COUNT = 1 @@ -30,6 +31,8 @@ internal object WalletImageResolver { cardTypesResolver.isTraillantWallet() -> userWallet.resolveTraillantWallet() cardTypesResolver.isTronWallet() -> userWallet.resolveTronWallet() cardTypesResolver.isKaspaWallet() -> userWallet.resolveKaspaWallet() + cardTypesResolver.isKaspa2Wallet() -> userWallet.resolveKaspa2Wallet() + cardTypesResolver.isKaspaResellerWallet() -> userWallet.resolveKaspaResellerWallet() cardTypesResolver.isBadWallet() -> userWallet.resolveBadWallet() cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() @@ -37,6 +40,15 @@ internal object WalletImageResolver { cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet() cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet() cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet() + cardTypesResolver.isRedPandaWallet() -> userWallet.resolveRedPandaWallet() + cardTypesResolver.isCryptoSethWallet() -> userWallet.resolveCryptoSethWallet() + cardTypesResolver.isKishuInuWallet() -> userWallet.resolveKishuInuWallet() + cardTypesResolver.isBabyDogeWallet() -> userWallet.resolveBabyDogeWallet() + cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet() + cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet() + cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet() + cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet() + cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet() cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() @@ -77,6 +89,20 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveKaspa2Wallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_kaspa2_card2_120_106, + twoBackupResId = R.drawable.ill_kaspa2_card3_120_106, + ) + } + + private fun UserWallet.resolveKaspaResellerWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_kaspa_reseller_card2_120_106, + twoBackupResId = R.drawable.ill_kaspa_reseller_card3_120_106, + ) + } + private fun UserWallet.resolveBadWallet(): Int? { return resolveWallet2( oneBackupResId = R.drawable.ill_bad_card2_120_106, @@ -154,6 +180,71 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveRedPandaWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_red_panda_card2_120_106, + twoBackupResId = R.drawable.ill_red_panda_card3_120_106, + ) + } + + private fun UserWallet.resolveCryptoSethWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_crypto_seth_card2_120_106, + twoBackupResId = R.drawable.ill_crypto_seth_card3_120_106, + ) + } + + private fun UserWallet.resolveKishuInuWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_kishu_inu_card2_120_106, + twoBackupResId = R.drawable.ill_kishu_inu_card3_120_106, + ) + } + + private fun UserWallet.resolveBabyDogeWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_baby_doge_card2_120_106, + twoBackupResId = R.drawable.ill_baby_doge_card3_120_106, + ) + } + + private fun UserWallet.resolveCOQWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_coq_card2_120_106, + twoBackupResId = R.drawable.ill_coq_card3_120_106, + ) + } + + private fun UserWallet.resolveCoinMetricaWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_coin_metrica_card2_120_106, + twoBackupResId = R.drawable.ill_coin_metrica_card3_120_106, + ) + } + + private fun UserWallet.resolveVoltInuWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_volt_inu_card2_120_106, + twoBackupResId = R.drawable.ill_volt_inu_card3_120_106, + ) + } + + private fun UserWallet.resolveVividWallet(): Int? { + // for multicolored cards use image of 3 cards in all cases + return resolveWallet2( + oneBackupResId = R.drawable.ill_vivid_cards3_120_106, + twoBackupResId = R.drawable.ill_vivid_cards3_120_106, + ) + } + + private fun UserWallet.resolvePastelWallet(): Int? { + // for multicolored cards use image of 3 cards in all cases + return resolveWallet2( + oneBackupResId = R.drawable.ill_pastel_cards3_120_106, + twoBackupResId = R.drawable.ill_pastel_cards3_120_106, + ) + } + private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { 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 new file mode 100644 index 0000000000..99e60b3682 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository +import timber.log.Timber + +class WalletNameMigrationUseCase( + private val userWalletsListManager: UserWalletsListManager, + 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) } + } + Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) + } + + walletNamesMigrationRepository.setMigrationDone() + } + + private fun suggestedWalletName(defaultName: String, existingNames: MutableSet): String { + val startIndex = 1 + for (index in startIndex..MAX_WALLETS_LIMIT) { + val name = if (index == startIndex) defaultName else "$defaultName $index" + + if (!existingNames.contains(name)) { + existingNames.add(name) + return name + } + } + + return defaultName + } + + companion object { + const val MAX_WALLETS_LIMIT = 10000 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index bc6d40794b..0a3310ce92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory @@ -13,7 +13,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsCheck import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletConnectNetworksSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents @@ -29,7 +28,7 @@ internal class MultiWalletContentLoader( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val reduxStateHolder: ReduxStateHolder, + private val walletFeatureToggles: WalletFeatureToggles, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -43,6 +42,7 @@ internal class MultiWalletContentLoader( walletWithFundsChecker = walletWithFundsChecker, getTokenListUseCase = getTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + walletFeatureToggles = walletFeatureToggles, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), @@ -53,11 +53,6 @@ internal class MultiWalletContentLoader( getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), - WalletConnectNetworksSubscriber( - userWallet = userWallet, - getTokenListUseCase = getTokenListUseCase, - reduxStateHolder = reduxStateHolder, - ), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 540fdea589..13dbadebb3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory @@ -25,8 +25,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val reduxStateHolder: ReduxStateHolder, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val walletFeatureToggles: WalletFeatureToggles, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) { @@ -40,9 +40,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( getTokenListUseCase = getTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - reduxStateHolder = reduxStateHolder, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, + walletFeatureToggles = walletFeatureToggles, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index b8aecf2d9f..03fb7f7902 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -25,10 +25,11 @@ internal sealed interface WalletAlertState { open val text: String = "" open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) abstract val onConfirmClick: (String) -> Unit + abstract val errorTextProvider: (String) -> TextReference? } data class DefaultAlert( - override val title: TextReference, + override val title: TextReference?, override val message: TextReference, override val onConfirmClick: (() -> Unit)?, ) : Basic() @@ -36,6 +37,7 @@ internal sealed interface WalletAlertState { data class RenameWalletAlert( override val text: String, override val onConfirmClick: (String) -> Unit, + override val errorTextProvider: (String) -> TextReference?, ) : TextInput() { override val title: TextReference = resourceReference(id = R.string.user_wallet_list_rename_popup_title) override val label: TextReference = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index 5ee98fda79..50a1af41c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -10,11 +10,9 @@ internal sealed class WalletEvent { data class ShowError(val text: TextReference) : WalletEvent() - data class ShowToast(val text: TextReference) : WalletEvent() - data class ShowAlert(val state: WalletAlertState) : WalletEvent() - data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() + data class CopyAddress(val address: String) : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index 124d3bf6a9..afbd667405 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -18,36 +18,53 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { /** Is click enabled */ abstract val enabled: Boolean + /** Whether to dim content */ + abstract val dimContent: Boolean + /** Lambda be invoked when manage button is clicked */ abstract val onClick: () -> Unit + /** Lambda be invoked when manage button is long clicked */ + open val onLongClick: (() -> TextReference?)? = null + /** * Buy * * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Buy button is clicked */ - data class Buy(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_buy), - iconResId = R.drawable.ic_plus_24, - onClick = onClick, - enabled = enabled, - ), - ) + data class Buy( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : + WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_buy), + iconResId = R.drawable.ic_plus_24, + onClick = onClick, + dimContent = dimContent, + ), + ) /** * Send * * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Send button is clicked */ - data class Send(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( + data class Send( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) @@ -55,13 +72,20 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { * Receive * * @property onClick lambda be invoked when Receive button is clicked + * @property onLongClick lambda be invoked when Receive button is long clicked */ - data class Receive(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( + data class Receive( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + override val onLongClick: (() -> TextReference?)?, + ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, + dimContent = dimContent, onClick = onClick, - enabled = enabled, + onLongClick = onLongClick, ), ) @@ -69,14 +93,19 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { * Sell * * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Sell button is clicked */ - data class Sell(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( + data class Sell( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_sell), iconResId = R.drawable.ic_currency_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) @@ -84,14 +113,19 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { * Swap * * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Swap button is clicked */ - data class Swap(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( + data class Swap( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt index ca91846374..8bf0e4ea63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt @@ -6,4 +6,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model * @property isRefreshing state is indicator visible * @property onRefresh lambda be invoked when pulled to refresh */ -data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file +data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: (ShowRefreshState) -> Unit) { + + @JvmInline + value class ShowRefreshState( + val value: Boolean, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index e861c77579..62f9ca68a1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -10,14 +10,14 @@ import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class WalletTokensListState { - object Empty : WalletTokensListState() + data object Empty : WalletTokensListState() sealed class ContentState : WalletTokensListState() { abstract val items: ImmutableList abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig? - object Loading : ContentState() { + data object Loading : ContentState() { override val items = persistentListOf() override val organizeTokensButtonConfig = null } @@ -27,7 +27,7 @@ internal sealed class WalletTokensListState { override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, ) : ContentState() - object Locked : ContentState() { + data object Locked : ContentState() { override val items = persistentListOf( TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 73065cb197..0ead18d878 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -85,10 +85,10 @@ internal class InitializeWalletsTransformer( private fun createDisabledButtons(): PersistentList { return persistentListOf( - WalletManageButton.Buy(enabled = false, onClick = {}), - WalletManageButton.Send(enabled = false, onClick = {}), - WalletManageButton.Receive(enabled = false, onClick = {}), - WalletManageButton.Sell(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, dimContent = false, onClick = {}, onLongClick = null), + WalletManageButton.Send(enabled = false, dimContent = false, onClick = {}), + WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}), + WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt index e7fedeba18..c541164b5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton @@ -43,26 +44,50 @@ internal class SetCryptoCurrencyActionsTransformer( when (action) { is TokenActionsState.ActionState.Buy -> { WalletManageButton.Buy( - enabled = action.enabled, - onClick = { clickIntents.onBuyClick(cryptoCurrencyStatus) }, + enabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { + clickIntents.onBuyClick( + cryptoCurrencyStatus = cryptoCurrencyStatus, + unavailabilityReason = action.unavailabilityReason, + ) + }, ) } is TokenActionsState.ActionState.Receive -> { WalletManageButton.Receive( - enabled = action.enabled, - onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }, + enabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { + clickIntents.onReceiveClick(cryptoCurrencyStatus = cryptoCurrencyStatus) + }, + onLongClick = { + clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) + }, ) } is TokenActionsState.ActionState.Sell -> { WalletManageButton.Sell( - enabled = action.enabled, - onClick = { clickIntents.onSellClick(cryptoCurrencyStatus) }, + enabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { + clickIntents.onSellClick( + cryptoCurrencyStatus = cryptoCurrencyStatus, + unavailabilityReason = action.unavailabilityReason, + ) + }, ) } is TokenActionsState.ActionState.Send -> { WalletManageButton.Send( - enabled = action.enabled, - onClick = { clickIntents.onSendClick(cryptoCurrencyStatus) }, + enabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { + clickIntents.onSendClick( + cryptoCurrencyStatus = cryptoCurrencyStatus, + unavailabilityReason = action.unavailabilityReason, + ) + }, ) } else -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 1e1d944584..6a914a5c2a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount @@ -10,16 +10,16 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter internal class MultiWalletCardStateConverter( - private val fiatBalance: TokenList.FiatBalance, + private val fiatBalance: TotalFiatBalance, private val selectedWallet: UserWallet, private val appCurrency: AppCurrency, ) : Converter { override fun convert(value: WalletCardState): WalletCardState { return when (fiatBalance) { - is TokenList.FiatBalance.Loading -> value.toLoadingState() - is TokenList.FiatBalance.Failed -> value.toErrorState() - is TokenList.FiatBalance.Loaded -> value.toWalletCardState(fiatBalance) + is TotalFiatBalance.Loading -> value.toLoadingState() + is TotalFiatBalance.Failed -> value.toErrorState() + is TotalFiatBalance.Loaded -> value.toWalletCardState(fiatBalance) } } @@ -45,7 +45,7 @@ internal class MultiWalletCardStateConverter( ) } - private fun WalletCardState.toWalletCardState(fiatBalance: TokenList.FiatBalance.Loaded): WalletCardState { + private fun WalletCardState.toWalletCardState(fiatBalance: TotalFiatBalance.Loaded): WalletCardState { return WalletCardState.Content( id = id, title = title, 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 0e76d03e34..fa7478a428 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,6 +3,7 @@ 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.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.wallets.models.UserWallet @@ -51,7 +52,7 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Buy -> { title = resourceReference(R.string.common_buy) icon = R.drawable.ic_plus_24 - action = { clickIntents.onBuyClick(cryptoCurrencyStatus) } + action = { clickIntents.onBuyClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) } } is TokenActionsState.ActionState.Receive -> { title = resourceReference(R.string.common_receive) @@ -61,17 +62,17 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) icon = R.drawable.ic_currency_24 - action = { clickIntents.onSellClick(cryptoCurrencyStatus) } + action = { clickIntents.onSellClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) } } is TokenActionsState.ActionState.Send -> { title = resourceReference(R.string.common_send) icon = R.drawable.ic_arrow_up_24 - action = { clickIntents.onSendClick(cryptoCurrencyStatus) } + action = { clickIntents.onSendClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) } } is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) icon = R.drawable.ic_exchange_horizontal_24 - action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } + action = { clickIntents.onSwapClick(cryptoCurrencyStatus, ScenarioUnavailabilityReason.None) } } is TokenActionsState.ActionState.CopyAddress -> { title = resourceReference(R.string.common_copy_address) @@ -90,7 +91,7 @@ internal class MultiWalletCurrencyActionsConverter( iconResId = icon, onClick = action, isWarning = actionsState is TokenActionsState.ActionState.HideToken, - enabled = actionsState.enabled, + enabled = actionsState.unavailabilityReason == ScenarioUnavailabilityReason.None, ) } } \ No newline at end of file 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 028d2ea966..775957b478 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 @@ -10,7 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter internal class SingleWalletCardStateConverter( - private val status: CryptoCurrencyStatus.Status, + private val status: CryptoCurrencyStatus.Value, private val selectedWallet: UserWallet, private val appCurrency: AppCurrency, ) : Converter { @@ -50,7 +50,7 @@ internal class SingleWalletCardStateConverter( ) } - private fun WalletCardState.toContentState(status: CryptoCurrencyStatus.Status): WalletCardState { + private fun WalletCardState.toContentState(status: CryptoCurrencyStatus.Value): WalletCardState { return WalletCardState.Content( id = id, title = title, @@ -66,7 +66,7 @@ internal class SingleWalletCardStateConverter( ) } - private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatFiatAmount( 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 157d0a1180..8d710656dc 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 @@ -10,7 +10,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( - private val status: CryptoCurrencyStatus.Status, + private val status: CryptoCurrencyStatus.Value, private val appCurrency: AppCurrency, ) : Converter { @@ -44,23 +44,23 @@ internal class SingleWalletMarketPriceConverter( ) } - private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatFiatAmount( + return BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = fiatRate, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) } - private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType { return PriceChangeConverter.fromBigDecimal(status.priceChange) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index aa168db655..f41d4d4ee7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -108,7 +108,7 @@ internal class TokenItemStateConverter( private fun BigDecimal.getFormattedCryptoPrice(): String { val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount( + return BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = this, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, 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 02c8d42705..ccb6834bde 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 @@ -6,6 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState @@ -79,7 +80,7 @@ internal class TokenListStateConverter( private fun getOrganizeTokensButtonState(currenciesSize: Int): WalletOrganizeTokensButtonConfig? { return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) { WalletOrganizeTokensButtonConfig( - isEnabled = tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading, + isEnabled = tokenList.totalFiatBalance !is TotalFiatBalance.Loading, onClick = clickIntents::onOrganizeTokensClick, ) } else { 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 866f9981a0..feeb07e488 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,7 +45,7 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn walletCardState = userWallet.toLoadingWalletCardState(), warnings = persistentListOf(), bottomSheetConfig = null, - buttons = createDisabledButtons(), + buttons = createDimmedButtons(), marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = currencySymbol), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -72,7 +72,7 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn } private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { - return WalletPullToRefreshConfig(onRefresh = clickIntents::onRefreshSwipe, isRefreshing = false) + return WalletPullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { @@ -86,12 +86,12 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn ) } - private fun createDisabledButtons(): PersistentList { + private fun createDimmedButtons(): PersistentList { return persistentListOf( - WalletManageButton.Buy(enabled = false, onClick = {}), - WalletManageButton.Send(enabled = false, onClick = {}), - WalletManageButton.Receive(enabled = false, onClick = {}), - WalletManageButton.Sell(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Send(enabled = true, dimContent = true, onClick = {}), + WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), + WalletManageButton.Sell(enabled = true, dimContent = true, onClick = {}), ) } } \ No newline at end of file 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 7dcd29e739..c9bd3afc92 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 @@ -1,9 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase 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.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.NetworkGroup @@ -25,8 +27,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber -internal typealias MaybeTokenListFlow = Flow> - @Suppress("LongParameterList") internal abstract class BasicTokenListSubscriber( private val userWallet: UserWallet, @@ -41,7 +41,7 @@ internal abstract class BasicTokenListSubscriber( private val sendAnalyticsJobHolder = JobHolder() private val onTokenListReceivedJobHolder = JobHolder() - protected abstract fun tokenListFlow(): MaybeTokenListFlow + protected abstract fun tokenListFlow(): LceFlow override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( @@ -61,11 +61,24 @@ internal abstract class BasicTokenListSubscriber( }, flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(), transform = { maybeTokenList, maybeAppCurrency -> - val tokenList = maybeTokenList.getOrElse { e -> - Timber.e("Failed to load token list: $e") - SetTokenListErrorTransformer(userWallet.walletId, e) - return@combine - } + val tokenList = maybeTokenList.getOrElse( + ifLoading = { maybeContent -> + val isRefreshing = stateHolder.getWalletState(userWallet.walletId) + ?.pullToRefreshConfig + ?.isRefreshing + ?: false + + maybeContent + ?.takeIf { !isRefreshing } + ?: return@combine + }, + ifError = { e -> + Timber.e("Failed to load token list: $e") + SetTokenListErrorTransformer(userWallet.walletId, e) + return@combine + }, + ) + val appCurrency = maybeAppCurrency.getOrElse { e -> Timber.e("Failed to load app currency: $e") AppCurrency.Default @@ -77,7 +90,7 @@ internal abstract class BasicTokenListSubscriber( ) } - private suspend fun startCheck(maybeTokenList: Either) { + private suspend fun startCheck(maybeTokenList: Lce) { // Run Polkadot account health check maybeTokenList.getOrNull()?.let { tokenList -> val cryptoCurrencies = when (tokenList) { @@ -92,17 +105,17 @@ internal abstract class BasicTokenListSubscriber( } } - protected open suspend fun onTokenListReceived(maybeTokenList: Either) { + protected open suspend fun onTokenListReceived(maybeTokenList: Lce) { /* no-op */ } - private suspend fun sendTokenListAnalytics(maybeTokenList: Either) { + private suspend fun sendTokenListAnalytics(maybeTokenList: Lce) { val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) tokenListAnalyticsSender.send( displayedUiState = displayedState, userWallet = userWallet, - tokenList = maybeTokenList.getOrElse { return }, + tokenList = maybeTokenList.getOrNull() ?: return, ) } 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 e3de1238a9..6b19dfd81d 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 @@ -1,25 +1,30 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.coroutines.flow.map @Suppress("LongParameterList") internal class MultiWalletTokenListSubscriber( private val userWallet: UserWallet, private val getTokenListUseCase: GetTokenListUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, + private val walletFeatureToggles: WalletFeatureToggles, stateHolder: WalletStateController, clickIntents: WalletClickIntents, tokenListAnalyticsSender: TokenListAnalyticsSender, @@ -36,17 +41,20 @@ internal class MultiWalletTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId) - - override suspend fun onTokenListReceived(maybeTokenList: Either) { - // TODO disabled for 5.7.2 because of potential critical - // updateSortingIfNeeded(maybeTokenList) + override fun tokenListFlow(): LceFlow { + return if (walletFeatureToggles.isTokenListLceFlowEnabled) { + getTokenListUseCase.launchLce(userWallet.walletId) + } else { + getTokenListUseCase.launch(userWallet.walletId).map { it.toLce() } + } } - @Suppress("UnusedPrivateMember") - private suspend fun updateSortingIfNeeded(maybeTokenList: Either) { - val tokenList = maybeTokenList.getOrElse { return } - if (!checkNeedSorting(tokenList)) return + override suspend fun onTokenListReceived(maybeTokenList: Lce) { + updateSortingIfNeeded(maybeTokenList) + } + + private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) { + val tokenList = getTokenList(maybeTokenList) ?: return applyTokenListSortingUseCase( userWalletId = userWallet.walletId, @@ -56,9 +64,14 @@ internal class MultiWalletTokenListSubscriber( ) } - private fun checkNeedSorting(tokenList: TokenList): Boolean { - return tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading && - tokenList.sortedBy == TokenList.SortType.BALANCE + private fun getTokenList(lce: Lce<*, TokenList>): TokenList? { + val tokenList = lce.getOrNull(isPartialContentAccepted = false) + ?: return null + + return tokenList.takeIf { + tokenList.totalFiatBalance is TotalFiatBalance.Loaded && + tokenList.sortedBy == TokenList.SortType.BALANCE + } } private fun getCurrenciesIds(tokenList: TokenList): List { 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 952fcb8c29..886cc1495e 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 @@ -1,13 +1,18 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.coroutines.flow.map @Suppress("LongParameterList") internal class SingleWalletWithTokenListSubscriber( @@ -29,5 +34,6 @@ internal class SingleWalletWithTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): MaybeTokenListFlow = getCardTokensListUseCase(userWallet.walletId) + override fun tokenListFlow(): LceFlow = getCardTokensListUseCase(userWallet.walletId) + .map { it.toLce() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletConnectNetworksSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletConnectNetworksSubscriber.kt deleted file mode 100644 index 420a93771a..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletConnectNetworksSubscriber.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import arrow.core.Either -import com.tangem.domain.redux.ReduxStateHolder -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.NetworkGroup -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.walletconnect.WalletConnectActions -import com.tangem.domain.wallets.models.UserWallet -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import timber.log.Timber - -/** - * WalletConnect networks subscriber. Update WalletConnect networks for a specified [userWallet]. - * - * @property userWallet user wallet - * @property getTokenListUseCase use case for subscribing on token list changes - * @property reduxStateHolder redux state holder - * -[REDACTED_AUTHOR] - */ -internal class WalletConnectNetworksSubscriber( - private val userWallet: UserWallet, - private val getTokenListUseCase: GetTokenListUseCase, - private val reduxStateHolder: ReduxStateHolder, -) : WalletSubscriber() { - - private val mutex = Mutex() - - override fun create(coroutineScope: CoroutineScope): Flow> { - return getTokenListUseCase(userWalletId = userWallet.walletId) - .conflate() - .distinctUntilCurrenciesChanged() - .filterLoadedTokens() - .onEach { - mutex.withLock { - Timber.d("WalletConnect: ${userWallet.walletId} networks is updated") - - reduxStateHolder.dispatch( - action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet), - ) - } - } - } - - private fun MaybeTokenListFlow.distinctUntilCurrenciesChanged(): MaybeTokenListFlow { - return distinctUntilChanged { old, new -> - val oldCurrencies = old.fold(ifLeft = { null }, ifRight = { it.getCryptoCurrencies() }) - val newCurrencies = new.fold(ifLeft = { null }, ifRight = { it.getCryptoCurrencies() }) - - oldCurrencies == newCurrencies - } - } - - private fun MaybeTokenListFlow.filterLoadedTokens(): MaybeTokenListFlow { - return filter { either -> - either.fold( - ifLeft = { false }, - ifRight = { it.getCryptoCurrencies().isAllCurrenciesLoaded() }, - ) - } - } - - private fun TokenList.getCryptoCurrencies(): List { - return when (this) { - is TokenList.Ungrouped -> currencies - is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies) - else -> emptyList() - } - } - - private fun List.isAllCurrenciesLoaded(): Boolean { - return none { it.value is CryptoCurrencyStatus.Loading } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index c5303e085e..48e959ad54 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -64,7 +64,9 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U fieldValue = value, confirmButton = DialogButton( title = state.confirmButtonText.resolveReference(), - enabled = value.text.isNotEmpty() && value.text != state.text, + enabled = value.text.isNotEmpty() && + value.text != state.text && + state.errorTextProvider(value.text) == null, onClick = { state.onConfirmClick(value.text) onDismiss() @@ -76,6 +78,8 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), textFieldParams = AdditionalTextInputDialogParams( label = state.label.resolveReference(), + isError = state.errorTextProvider(value.text) != null, + caption = state.errorTextProvider(value.text)?.resolveReference(), ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index cce277150d..19cd88f97f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui -import android.widget.Toast import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -11,6 +11,7 @@ import androidx.compose.ui.text.AnnotatedString import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester @@ -42,12 +43,12 @@ internal fun WalletEventEffect( is WalletEvent.ShowError -> { snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) } - is WalletEvent.ShowToast -> { - Toast.makeText(context, value.text.resolveReference(resources), Toast.LENGTH_SHORT).show() - } is WalletEvent.CopyAddress -> { clipboardManager.setText(AnnotatedString(value.address)) - Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() + snackbarHostState.showSnackbar( + message = resources.getString(R.string.wallet_notification_address_copied), + duration = SnackbarDuration.Short, + ) } is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) is WalletEvent.RateApp -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 9819f09790..c930c63241 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.animateFloatAsState @@ -22,9 +23,14 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics +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 androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.components.Keyboard @@ -37,9 +43,15 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet @@ -126,16 +138,17 @@ private fun WalletContent( mutableStateOf(value = lazyTxHistoryItems) } - val betweenItemsPadding = TangemTheme.dimens.spacing14 + val betweenItemsPadding = TangemTheme.dimens.spacing12 val horizontalPadding = TangemTheme.dimens.spacing16 val itemModifier = movableItemModifier .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(TestTags.WALLET_SCREEN), contentPadding = PaddingValues( - top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing92, ), horizontalAlignment = Alignment.CenterHorizontally, @@ -284,7 +297,11 @@ private fun BaseScaffoldManageTokenRedesign( BottomSheetScaffold( snackbarHost = { - SnackbarHost(hostState = snackbarHostState) + WalletSnackbarHost( + snackbarHostState = it, + event = state.event, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + ) }, containerColor = TangemTheme.colors.background.secondary, sheetContainerColor = TangemTheme.colors.background.primary, @@ -315,7 +332,9 @@ private fun BaseScaffoldManageTokenRedesign( content = { paddingValues -> val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, ) Column( @@ -486,7 +505,13 @@ private fun BaseScaffold( ) { Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHost = { + WalletSnackbarHost( + snackbarHostState = snackbarHostState, + event = state.event, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + ) + }, floatingActionButton = { val manageTokensButtonConfig by remember(state.selectedWalletIndex) { mutableStateOf( @@ -501,7 +526,9 @@ private fun BaseScaffold( content = { val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, ) Box( @@ -521,6 +548,21 @@ private fun BaseScaffold( ) } +@Composable +private fun WalletSnackbarHost( + snackbarHostState: SnackbarHostState, + event: StateEvent, + modifier: Modifier = Modifier, +) { + SnackbarHost(hostState = snackbarHostState, modifier = modifier) { data -> + if (event is StateEvent.Triggered && event.data is WalletEvent.CopyAddress) { + CopiedTextSnackbar(data) + } else { + TangemSnackbar(data) + } + } +} + @Composable private fun ManageTokensButton(onClick: () -> Unit) { PrimaryButton( @@ -544,4 +586,27 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi } } } -} \ No newline at end of file +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider::class) data: WalletScreenState) { + TangemThemePreview { + WalletScreen( + state = data, + bottomSheetHeaderHeightProvider = { 0.dp }, + bottomSheetContent = {}, + ) + } +} + +private class WalletScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + walletScreenState, + walletScreenState.copy(selectedWalletIndex = 1), + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index 865a52268f..5e4db0ab08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable @@ -14,6 +15,7 @@ import com.tangem.core.ui.components.getDefaultRowColors import com.tangem.core.ui.components.getWarningRowColors import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig @@ -49,24 +51,13 @@ private fun ActionsBottomSheetContent(actions: ImmutableList( - collection = listOf(WalletPreviewData.bottomSheet), -) \ No newline at end of file +private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( + collection = listOf(WalletPreviewData.bottomSheet.content as WalletBottomSheetConfig), +) +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index eed63a9195..3962d07d47 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.* @@ -49,6 +50,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState @@ -195,7 +197,7 @@ private fun CardContainer( ConstraintLayout( modifier = Modifier .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing14), + .padding(horizontal = TangemTheme.dimens.spacing12), ) { content(itemSize) } @@ -387,20 +389,13 @@ private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_WalletCard_LightTheme( +private fun Preview_WalletCard( @PreviewParameter(WalletCardStateProvider::class) state: WalletCardState, ) { - TangemTheme(isDark = false) { - WalletCard(state = state, isBalanceHidden = false) - } -} - -@Preview -@Composable -private fun Preview_WalletCard_DarkTheme(@PreviewParameter(WalletCardStateProvider::class) state: WalletCardState) { - TangemTheme(isDark = true) { + TangemThemePreview { WalletCard(state = state, isBalanceHidden = false) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index 4813eaff9f..c8e9a69c9d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -1,10 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common +import android.content.res.Configuration import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig @@ -36,17 +38,10 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_WalletTopBar_LightTheme() { - TangemTheme(isDark = false) { - WalletTopBar(config = WalletPreviewData.topBarConfig) - } -} - -@Preview -@Composable -private fun Preview_WalletTopBar_DarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_WalletTopBar() { + TangemThemePreview { WalletTopBar(config = WalletPreviewData.topBarConfig) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt index 332d7e3209..a37bf1f17f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.visa +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState @@ -158,21 +160,12 @@ private fun AvailableLimit(availableBalance: String, limitDays: Int, modifier: M // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BalancesAndLimitsBlockPreview_Light( +private fun BalancesAndLimitsBlockPreview( @PreviewParameter(BalancesAndLimitsBlockParameterProvider::class) state: BalancesAndLimitsBlockState, ) { - TangemTheme { - BalancesAndLimitsBlock(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BalancesAndLimitsBlockPreview_Dark( - @PreviewParameter(BalancesAndLimitsBlockParameterProvider::class) state: BalancesAndLimitsBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { BalancesAndLimitsBlock(state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt index b4a5b96ffc..97dd93b282 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.visa +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBottomSheetConfig @@ -159,21 +161,12 @@ private inline fun ContentContainer( // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BalancesAndLimitsBottomSheetPreview_Light( +private fun BalancesAndLimitsBottomSheetPreview( @PreviewParameter(BalancesAndLimitsBottomSheetParameterProvider::class) state: BalancesAndLimitsBottomSheetConfig, ) { - TangemTheme { - BalancesAndLimitsContent(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BalancesAndLimitsBottomSheetPreview_Dark( - @PreviewParameter(BalancesAndLimitsBottomSheetParameterProvider::class) state: BalancesAndLimitsBottomSheetConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { BalancesAndLimitsContent(state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt index eafdaf3801..441f6e8c56 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.visa +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,6 +20,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import kotlinx.collections.immutable.persistentListOf @@ -213,21 +215,12 @@ private fun ContentContainer( // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun VisaTxDetailsBottomSheetPreview_Light( +private fun VisaTxDetailsBottomSheetPreview( @PreviewParameter(VisaTxDetailsBottomSheetParameterProvider::class) state: VisaTxDetailsBottomSheetConfig, ) { - TangemTheme { - VisaTxDetailsBottomSheetContent(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun VisaTxDetailsBottomSheetPreview_Dark( - @PreviewParameter(VisaTxDetailsBottomSheetParameterProvider::class) state: VisaTxDetailsBottomSheetConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { VisaTxDetailsBottomSheetContent(state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt index 5ebccf6c11..f2c721ac87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/ScreenLifecycleProvider.kt @@ -3,19 +3,21 @@ package com.tangem.feature.wallet.presentation.wallet.utils import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @ViewModelScoped internal class ScreenLifecycleProvider @Inject constructor() : DefaultLifecycleObserver { - var isBackground: Boolean = true - private set + private val _isBackgroundState = MutableStateFlow(false) + val isBackgroundState: StateFlow = _isBackgroundState override fun onResume(owner: LifecycleOwner) { - isBackground = false + _isBackgroundState.value = false } override fun onPause(owner: LifecycleOwner) { - isBackground = true + _isBackgroundState.value = true } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 369ec32691..dbad1dde46 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -5,23 +5,23 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.AppScreen import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase -import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender @@ -32,6 +32,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.extensions.indexOfFirstOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -56,25 +57,35 @@ internal class WalletViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, + private val walletNameMigrationUseCase: WalletNameMigrationUseCase, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState private lateinit var router: InnerWalletRouter - private var walletsUpdateJobHolder: JobHolder = JobHolder() + private val walletsUpdateJobHolder = JobHolder() + private val refreshWalletJobHolder = JobHolder() + private var needToRefreshWallet = false init { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) suggestToEnableBiometrics() + maybeMigrateNames() subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() + subscribeToScreenBackgroundState() + } + + private fun maybeMigrateNames() { + viewModelScope.launch { + walletNameMigrationUseCase() + } } fun setWalletRouter(router: InnerWalletRouter) { @@ -141,29 +152,62 @@ internal class WalletViewModel @Inject constructor( .distinctUntilChanged() .onEach { selectedWallet -> if (selectedWallet.isMultiCurrency) { - Timber.d("WalletConnect: initialize and setup networks for ${selectedWallet.walletId}") - - reduxStateHolder.dispatch( - action = WalletConnectActions.New.Initialize(userWallet = selectedWallet), - ) - - reduxStateHolder.dispatch( - action = WalletConnectActions.New.SetupUserChains(userWallet = selectedWallet), - ) - selectedWalletAnalyticsSender.send(selectedWallet) } - walletDeepLinksHandler.registerForWallet( - viewModel = this, - userWallet = selectedWallet, - ) + changeSelectedWalletState(selectedWalletId = selectedWallet.walletId) + + walletDeepLinksHandler.registerForWallet(viewModel = this, userWallet = selectedWallet) } .flowOn(dispatchers.main) .launchIn(viewModelScope) } } + /** Change selected wallet state if selected wallet [selectedWalletId] was changed in the background */ + private suspend fun changeSelectedWalletState(selectedWalletId: UserWalletId) { + if (screenLifecycleProvider.isBackgroundState.value && selectedWalletId != stateHolder.getSelectedWalletId()) { + stateHolder.value.wallets + .indexOfFirstOrNull { prevState -> prevState.walletCardState.id == selectedWalletId } + ?.let { selectedIndex -> + Timber.e("Selected wallet changed from background state: $selectedWalletId") + + delay(timeMillis = 1000) + scrollToWallet(selectedIndex) + } + } + } + + // We need to update the current wallet if the application was in the background for more than 10 seconds + // and then returned to the foreground + private fun subscribeToScreenBackgroundState() { + screenLifecycleProvider.isBackgroundState + .onEach { isBackground -> + refreshWalletJobHolder.cancel() + when { + isBackground -> needToRefreshTimer() + needToRefreshWallet && !isBackground -> triggerRefreshWallet() + } + } + .launchIn(viewModelScope) + } + + private fun needToRefreshTimer() { + viewModelScope.launch { + delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) + needToRefreshWallet = true + }.saveIn(refreshWalletJobHolder) + } + + private fun triggerRefreshWallet() { + needToRefreshWallet = false + val state = stateHolder.uiState.value + val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return + wallet.pullToRefreshConfig.onRefresh.invoke( + WalletPullToRefreshConfig.ShowRefreshState(false), + ) + } + private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) @@ -177,9 +221,9 @@ internal class WalletViewModel @Inject constructor( is WalletsUpdateActionResolver.Action.UpdateWalletName -> { stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) } - is WalletsUpdateActionResolver.Action.NoAccessibleWallets -> closeScreen(screen = AppScreen.Welcome) - is WalletsUpdateActionResolver.Action.NoWallets -> closeScreen(screen = AppScreen.Home) - is WalletsUpdateActionResolver.Action.Unknown -> Unit + is WalletsUpdateActionResolver.Action.Unknown -> { + Timber.w("Unable to perfom action: $action") + } } } @@ -245,7 +289,7 @@ internal class WalletViewModel @Inject constructor( ), ) - withContext(dispatchers.io) { delay(timeMillis = 700) } + withContext(dispatchers.io) { delay(timeMillis = 1000) } scrollToWallet(index = action.selectedWalletIndex) } @@ -258,21 +302,27 @@ internal class WalletViewModel @Inject constructor( ) /* - * If card is reset to factory settings, then Compose need some time to draw the WalletScreen. - * Otherwise, scroll isn't happened + * Should not show scroll animation if WalletScreen isn't in the background. + * Example, reset card */ - withContext(dispatchers.io) { delay(timeMillis = 1000) } + if (screenLifecycleProvider.isBackgroundState.value) { + updateStateByDeleteWalletTransformer(action) + } else { + withContext(dispatchers.io) { delay(timeMillis = 1000) } - scrollToWallet( - index = action.selectedWalletIndex, - onConsume = { - stateHolder.update( - DeleteWalletTransformer( - selectedWalletIndex = action.selectedWalletIndex, - deletedWalletId = action.deletedWalletId, - ), - ) - }, + scrollToWallet( + index = action.selectedWalletIndex, + onConsume = { updateStateByDeleteWalletTransformer(action) }, + ) + } + } + + private fun updateStateByDeleteWalletTransformer(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + stateHolder.update( + DeleteWalletTransformer( + selectedWalletIndex = action.selectedWalletIndex, + deletedWalletId = action.deletedWalletId, + ), ) } @@ -293,13 +343,6 @@ internal class WalletViewModel @Inject constructor( ) } - private fun closeScreen(screen: AppScreen) { - if (!screenLifecycleProvider.isBackground) { - stateHolder.clear() - router.popBackStack(screen = screen) - } - } - private fun scrollToWallet(index: Int, onConsume: () -> Unit = {}) { stateHolder.update( ScrollToWalletTransformer( @@ -310,4 +353,8 @@ internal class WalletViewModel @Inject constructor( ), ) } + + private companion object { + const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 94b310fd4a..aeb7ea5c03 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import arrow.core.getOrElse import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -23,16 +24,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( ) { fun resolve(wallets: List, currentState: WalletScreenState): Action { - val selectedWallet = wallets.getSelectedWallet() + val selectedWallet = getSelectedWalletSyncUseCase().getOrElse { + /* Selected user wallet can be null after reset if remaining user wallets is locked */ + return Action.Unknown + } - val action = if (selectedWallet == null) { - createNoSelectedWalletAction(wallets) + val action = if (isFirstInitialization(currentState)) { + createInitializeWalletsAction(wallets, selectedWallet) } else { - if (isFirstInitialization(currentState)) { - createInitializeWalletsAction(wallets, selectedWallet) - } else { - getUpdateContentAction(currentState, wallets, selectedWallet) - } + getUpdateContentAction(currentState, wallets, selectedWallet) } Timber.d("Resolved action: $action") @@ -40,22 +40,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( return action } - private fun List.getSelectedWallet(): UserWallet? { - return when { - isEmpty() -> null - size == 1 -> if (first().isLocked) null else first() - else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it }) - } - } - - private fun createNoSelectedWalletAction(wallets: List): Action { - return when { - wallets.isEmpty() -> Action.NoWallets - wallets.all(UserWallet::isLocked) -> Action.NoAccessibleWallets - else -> Action.Unknown - } - } - private fun isFirstInitialization(state: WalletScreenState): Boolean { return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX } @@ -289,10 +273,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( } } - data object NoAccessibleWallets : Action() - - data object NoWallets : Action() - data object Unknown : Action() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 10d52a0047..5b4bf78f5d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,13 +1,21 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.wallets.usecase.GetWalletNamesUseCase +import com.tangem.domain.wallets.usecase.RenameWalletUseCase +import com.tangem.feature.wallet.impl.R import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -36,32 +44,48 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, + private val renameWalletUseCase: RenameWalletUseCase, + private val getWalletNamesUseCase: GetWalletNamesUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val reduxStateHolder: ReduxStateHolder, + private val reduxNavController: ReduxNavController, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) { analyticsEventHandler.send(MainScreen.EditWalletTapped) - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.RenameWalletAlert( - text = stateHolder.getSelectedWallet().walletCardState.title, - onConfirmClick = { onRenameAfterConfirmationClick(userWalletId, it) }, + viewModelScope.launch(dispatchers.main) { + val walletNames = getWalletNamesUseCase() + val currentWalletName = stateHolder.getSelectedWallet().walletCardState.title + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.RenameWalletAlert( + text = currentWalletName, + onConfirmClick = { onRenameAfterConfirmationClick(userWalletId, it) }, + errorTextProvider = { enteredName -> + if (walletNames.contains(enteredName) && enteredName != currentWalletName) { + resourceReference( + R.string.user_wallet_list_rename_popup_error_already_exists, + wrappedList(enteredName), + ) + } else { + null + } + }, + ), ), - ), - ) + ) + } } override fun onRenameAfterConfirmationClick(userWalletId: UserWalletId, name: String) { viewModelScope.launch(dispatchers.main) { - updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name = name) }) + renameWalletUseCase(userWalletId = userWalletId, name) } } @@ -81,18 +105,26 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( viewModelScope.launch(dispatchers.main) { walletScreenContentLoader.cancel(userWalletId) - val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch + val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch + val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + Timber.e("Unable to delete user wallet: $it") + return@launch + } - deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId) - .onLeft { Timber.e(it.toString()) } + deleteSavedAccessCodesUseCase(cardId = walletToDelete.cardId).onLeft { + Timber.e("Unable to delete user wallet access code: $it") + } - deleteWalletUseCase(userWalletId) - .onRight { - getSelectedWalletSyncUseCase().getOrNull()?.let { - reduxStateHolder.onUserWalletSelected(it) - } + if (hasUserWallets) { + val selectedWallet = getSelectedWalletSyncUseCase().getOrElse { + error("Unable to find selected wallet: $it") } - .onLeft { Timber.e(it.toString()) } + + reduxStateHolder.onUserWalletSelected(selectedWallet) + } else { + stateHolder.clear() + reduxNavController.navigate(NavigationAction.PopBackTo(AppScreen.Home)) + } } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 3cf99441ab..3266406422 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -77,17 +77,17 @@ internal class WalletClickIntents @Inject constructor( } } - fun onRefreshSwipe() { + fun onRefreshSwipe(showRefreshState: Boolean) { when (stateHolder.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { analyticsEventHandler.send(PortfolioEvent.Refreshed) - refreshMultiCurrencyContent() + refreshMultiCurrencyContent(showRefreshState) } is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, -> { analyticsEventHandler.send(PortfolioEvent.Refreshed) - refreshSingleCurrencyContent() + refreshSingleCurrencyContent(showRefreshState) } is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, @@ -97,14 +97,14 @@ internal class WalletClickIntents @Inject constructor( } fun onReloadClick() { - refreshSingleCurrencyContent() + refreshSingleCurrencyContent(showRefreshState = true) } - private fun refreshMultiCurrencyContent() { + private fun refreshMultiCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update( - SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) viewModelScope.launch(dispatchers.main) { @@ -126,11 +126,11 @@ internal class WalletClickIntents @Inject constructor( // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() - private fun refreshSingleCurrencyContent() { + private fun refreshSingleCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update( - SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) viewModelScope.launch(dispatchers.main) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index cae3373a31..5c192d9a44 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -82,7 +82,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( ), ) } else { - router.openDetailsScreen() + router.openDetailsScreen(stateHolder.getSelectedWalletId()) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 243f372827..bc843df1e7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -2,13 +2,17 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.haptic.HapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver @@ -19,6 +23,7 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.walletmanager.WalletManagersFacade @@ -43,22 +48,24 @@ import javax.inject.Inject interface WalletCurrencyActionsClickIntents { - fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) + + fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) + + fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) + + fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? + fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - - fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - - fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onExploreClick() } @@ -80,15 +87,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val reduxStateHolder: ReduxStateHolder, + private val hapticManager: HapticManager, + private val clipboardManager: ClipboardManager, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { - override fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onSendClick( + cryptoCurrencyStatus: CryptoCurrencyStatus, + unavailabilityReason: ScenarioUnavailabilityReason, + ) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrencyStatus.currency.symbol), ) + if (handleUnavailabilityReason(unavailabilityReason)) return + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) viewModelScope.launch(dispatchers.main) { val maybeFeeCurrencyStatus = @@ -121,7 +135,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun sendToken( cryptoCurrency: CryptoCurrency.Token, - cryptoCurrencyStatus: CryptoCurrencyStatus.Status, + cryptoCurrencyStatus: CryptoCurrencyStatus.Value, feeCurrencyStatus: CryptoCurrencyStatus?, userWallet: UserWallet, ) { @@ -167,6 +181,18 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) } + override fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? { + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null + val cryptoCurrency = cryptoCurrencyStatus.currency + val addresses = networkAddress.availableAddresses.mapToAddressModels(cryptoCurrency).toImmutableList() + val defaultAddress = addresses.firstOrNull()?.value ?: return null + + hapticManager.vibrateMeduim() + clipboardManager.setText(text = defaultAddress) + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + return resourceReference(R.string.wallet_notification_address_copied) + } + private fun createReceiveBottomSheetContent( currency: CryptoCurrency, addresses: Set, @@ -198,11 +224,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( .find { it.type == AddressType.Default } ?.value ?.let { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + walletEventSender.send( - event = WalletEvent.CopyAddress( - address = it, - toast = resourceReference(R.string.wallet_notification_address_copied), - ), + event = WalletEvent.CopyAddress(address = it), ) } } @@ -238,6 +263,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( formatArgs = WrappedList( listOf( cryptoCurrencyStatus.currency.name, + cryptoCurrencyStatus.currency.symbol, cryptoCurrencyStatus.currency.network.name, ), ), @@ -264,7 +290,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( .fold( ifLeft = { walletEventSender.send( - event = WalletEvent.ShowToast(text = resourceReference(R.string.common_error)), + event = WalletEvent.ShowError(text = resourceReference(R.string.common_error)), ) }, ifRight = { @@ -274,11 +300,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onSellClick( + cryptoCurrencyStatus: CryptoCurrencyStatus, + unavailabilityReason: ScenarioUnavailabilityReason, + ) { analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrencyStatus.currency.symbol), ) + if (handleUnavailabilityReason(unavailabilityReason)) return + showErrorIfDemoModeOrElse { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( @@ -291,13 +322,18 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onBuyClick( + cryptoCurrencyStatus: CryptoCurrencyStatus, + unavailabilityReason: ScenarioUnavailabilityReason, + ) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrencyStatus.currency.symbol), ) + if (handleUnavailabilityReason(unavailabilityReason)) return + showErrorIfDemoModeOrElse { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( @@ -311,11 +347,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onSwapClick( + cryptoCurrencyStatus: CryptoCurrencyStatus, + unavailabilityReason: ScenarioUnavailabilityReason, + ) { analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), ) + if (handleUnavailabilityReason(unavailabilityReason)) return + reduxStateHolder.dispatch(TradeCryptoAction.Swap(cryptoCurrencyStatus.currency)) } @@ -402,4 +443,80 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( action() } } + + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { + if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false + + val unavailabilityReasonText = getUnavailabilityReasonText(unavailabilityReason) + + viewModelScope.launch(dispatchers.main) { + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.DefaultAlert( + title = null, + message = unavailabilityReasonText, + onConfirmClick = null, + ), + ), + ) + } + + return true + } + + private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { + return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.PendingTransaction -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_send, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + } + } + is ScenarioUnavailabilityReason.EmptyBalance -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_send, + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_sell, + ) + } + } + is ScenarioUnavailabilityReason.BuyUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_buy_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotExchangeable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotSupportedBySellService -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.Unreachable -> { + resourceReference( + id = R.string.token_button_unavailability_generic_description, + ) + } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) + ScenarioUnavailabilityReason.None -> { + throw IllegalArgumentException("The unavailability reason must be other than None") + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 418a4d8afd..21982d8010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -154,7 +154,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( val event = when (error) { is UnlockWalletsError.DataError, is UnlockWalletsError.UnableToUnlockWallets, - -> WalletEvent.ShowToast(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) + -> WalletEvent.ShowError(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) is UnlockWalletsError.NoUserWalletSelected, is UnlockWalletsError.NotAllUserWalletsUnlocked, -> WalletEvent.ShowAlert(WalletAlertState.RescanWallets) diff --git a/features/wallet/impl/src/main/res/drawable/ill_baby_doge_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_baby_doge_card2_120_106.webp new file mode 100644 index 0000000000..bce39da73f Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_baby_doge_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_baby_doge_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_baby_doge_card3_120_106.webp new file mode 100644 index 0000000000..4ff9c26b80 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_baby_doge_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_coin_metrica_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_coin_metrica_card2_120_106.webp new file mode 100644 index 0000000000..f643bedab8 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_coin_metrica_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_coin_metrica_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_coin_metrica_card3_120_106.webp new file mode 100644 index 0000000000..5138a42f01 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_coin_metrica_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_coq_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_coq_card2_120_106.webp new file mode 100644 index 0000000000..124dc17d02 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_coq_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_coq_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_coq_card3_120_106.webp new file mode 100644 index 0000000000..496b98b4c3 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_coq_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_seth_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_seth_card2_120_106.webp new file mode 100644 index 0000000000..cd6cf7cf0f Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_crypto_seth_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_seth_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_seth_card3_120_106.webp new file mode 100644 index 0000000000..6c7efa8fa9 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_crypto_seth_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_kaspa2_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_kaspa2_card2_120_106.webp new file mode 100644 index 0000000000..a9adee3779 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_kaspa2_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_kaspa2_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_kaspa2_card3_120_106.webp new file mode 100644 index 0000000000..d1fa485038 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_kaspa2_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_kaspa_reseller_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_kaspa_reseller_card2_120_106.webp new file mode 100644 index 0000000000..726d48f1f9 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_kaspa_reseller_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_kaspa_reseller_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_kaspa_reseller_card3_120_106.webp new file mode 100644 index 0000000000..7374ebe3d6 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_kaspa_reseller_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_kishu_inu_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_kishu_inu_card2_120_106.webp new file mode 100644 index 0000000000..f308876d94 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_kishu_inu_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_kishu_inu_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_kishu_inu_card3_120_106.webp new file mode 100644 index 0000000000..f839b222e1 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_kishu_inu_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp new file mode 100644 index 0000000000..1b024df94d Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_red_panda_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_red_panda_card2_120_106.webp new file mode 100644 index 0000000000..826886502a Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_red_panda_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_red_panda_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_red_panda_card3_120_106.webp new file mode 100644 index 0000000000..a66e0a3755 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_red_panda_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp new file mode 100644 index 0000000000..ecc36428ee Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_volt_inu_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_volt_inu_card2_120_106.webp new file mode 100644 index 0000000000..308bc9ecf9 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_volt_inu_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_volt_inu_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_volt_inu_card3_120_106.webp new file mode 100644 index 0000000000..5df3d2bc27 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_volt_inu_card3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6d75483fe7..88ac8d88ab 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -39,7 +39,6 @@ compose-lifecycle-runtime = "2.7.0" # region Other libraries amplitude = "2.36.1" -appsflyer = "6.5.1" armadillo = "0.9.0" coil = "2.1.0" compose-shimmer = "1.0.3" @@ -68,7 +67,7 @@ xmlShimmer = "1.1.3" zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" -arrow = "1.2.0" +arrow = "1.2.3" reactiveNetwork = "3.0.8" walletConnectCore = "1.18.0" walletConnectWeb3 = "1.11.0" @@ -82,13 +81,15 @@ swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" leakcanary = "2.13" +decompose = "2.2.2" +room = "2.6.1" markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.11-686" +tangemBlockchainSdk = "release-app_5.12-691" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.11-365" +tangemCardSdk = "release-app_5.12-367" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -121,6 +122,7 @@ firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = " google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +room = { id = "androidx.room", version.ref = "room" } [libraries] # region Classpath @@ -195,14 +197,15 @@ test-junit-android = { module = "androidx.test.ext:junit", version.ref = "junitA test-truth = { module = "com.google.truth:truth", version.ref = "truth" } test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } test-kaspresso = { module = "com.kaspersky.android-components:kaspresso", version.ref = "kaspresso" } -test-kaspresso-compose = { module = "com.kaspersky.android-components:kaspresso-compose-support", version.ref = "kaspresso-compose"} +test-kaspresso-compose = { module = "com.kaspersky.android-components:kaspresso-compose-support", version.ref = "kaspresso-compose" } test-compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-junit" } test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } +test-hilt = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +test-hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } # endregion Test # region Other amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" } -appsflyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsflyer" } armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } @@ -222,6 +225,7 @@ lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } material = { module = "com.google.android.material:material", version.ref = "googleMaterialComponent" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } @@ -250,5 +254,10 @@ camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } web3j-core = { module = "org.web3j:core", version.ref = "web3j" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } +decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } +decompose-ext-compose = { module = "com.arkivanov.decompose:extensions-compose-jetpack", version.ref = "decompose" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } # endregion Other diff --git a/jitpack.gradle b/jitpack.gradle deleted file mode 100644 index cd263447fd..0000000000 --- a/jitpack.gradle +++ /dev/null @@ -1,4 +0,0 @@ -ext.jitpackSdk = [ - group: 'com.github.Tangem', - version : '0.2.1', -] \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/AppVersionProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/AppVersionProvider.kt deleted file mode 100644 index 3b8e606e9c..0000000000 --- a/libs/auth/src/main/java/com/tangem/lib/auth/AppVersionProvider.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.lib.auth - -interface AppVersionProvider { - - fun getAppVersion(): String -} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/AuthBearerProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/AuthBearerProvider.kt deleted file mode 100644 index dbeac3aebe..0000000000 --- a/libs/auth/src/main/java/com/tangem/lib/auth/AuthBearerProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.lib.auth - -/** - * Provides auth for tangemTech API - */ -interface AuthBearerProvider { - - /** - * Returns api-key - */ - fun getApiKey(): String -} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/StakeKitAuthProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/StakeKitAuthProvider.kt new file mode 100644 index 0000000000..d6f3fac532 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/StakeKitAuthProvider.kt @@ -0,0 +1,6 @@ +package com.tangem.lib.auth + +interface StakeKitAuthProvider { + + fun getApiKey(): String +} \ No newline at end of file diff --git a/libs/blockchain-sdk/.gitignore b/libs/blockchain-sdk/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/libs/blockchain-sdk/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts new file mode 100644 index 0000000000..47b65e7313 --- /dev/null +++ b/libs/blockchain-sdk/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.libs.blockchain_sdk" +} + +dependencies { + + // region Core modules + implementation(projects.core.datasource) + implementation(projects.core.featuretoggles) + implementation(projects.core.utils) + // endregion + + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + + // region DI libraries + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + // endregion + + // region Other libraries + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + // endregion + + // region Firebase libraries + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.firebase.crashlytics) + // endregion + + // region Tangem libraries + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } + implementation(deps.tangem.card.core) + // endregion + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt new file mode 100644 index 0000000000..991f599242 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt @@ -0,0 +1,17 @@ +package com.tangem.blockchainsdk + +import com.tangem.blockchain.common.WalletManagerFactory + +/** + * Blockchain SDK components factory + * +[REDACTED_AUTHOR] + */ +interface BlockchainSDKFactory { + + /** Initialize components */ + suspend fun init() + + /** Get [WalletManagerFactory] synchronously */ + suspend fun getWalletManagerFactorySync(): WalletManagerFactory? +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt new file mode 100644 index 0000000000..6207c86026 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -0,0 +1,105 @@ +package com.tangem.blockchainsdk + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter +import com.tangem.blockchainsdk.converters.BlockchainSDKConfigConverter +import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.store.RuntimeStore +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.config.models.ConfigValueModel +import com.tangem.datasource.config.models.ProviderModel +import com.tangem.libs.blockchain_sdk.BuildConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber + +internal typealias BlockchainProvidersResponse = Map> +internal typealias BlockchainProviderTypes = Map> + +/** + * Implementation of Blockchain SDK components factory + * + * @property assetLoader asset loader + * @property blockchainProvidersResponseLoader blockchain providers response loader + * @property configStore blockchain sdk config store + * @property blockchainProviderTypesStore blockchain provider types store + * @property walletManagerFactoryCreator wallet manager factory creator + * +[REDACTED_AUTHOR] + */ +internal class DefaultBlockchainSDKFactory( + private val assetLoader: AssetLoader, + private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + private val configStore: RuntimeStore, + private val blockchainProviderTypesStore: RuntimeStore, + private val walletManagerFactoryCreator: WalletManagerFactoryCreator, + dispatchers: CoroutineDispatcherProvider, +) : BlockchainSDKFactory { + + private val mainScope = CoroutineScope(dispatchers.main) + + private val walletManagerFactory: Flow = createWalletManagerFactory() + + override suspend fun init() { + coroutineScope { + updateBlockchainSDKConfig() + updateBlockchainProviderTypes() + } + } + + override suspend fun getWalletManagerFactorySync(): WalletManagerFactory? = walletManagerFactory.firstOrNull() + + private fun createWalletManagerFactory(): Flow { + return combine( + flow = configStore.get(), + flow2 = blockchainProviderTypesStore.get(), + // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] + transform = walletManagerFactoryCreator::create, + ) + .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) + } + + private fun CoroutineScope.updateBlockchainSDKConfig() { + launch { + val config = assetLoader.load(fileName = CONFIG_FILE_NAME) + + if (config == null) { + Timber.e("Error loading BlockchainSDKConfig") + return@launch + } + + Timber.d("Update BlockchainSDKConfig") + + configStore.store( + value = BlockchainSDKConfigConverter.convert(value = config), + ) + } + } + + private fun CoroutineScope.updateBlockchainProviderTypes() { + launch { + val response = blockchainProvidersResponseLoader.load() + + if (response == null) { + Timber.e("Error loading BlockchainProviderTypes") + return@launch + } + + Timber.d("Update BlockchainProviderTypes") + + blockchainProviderTypesStore.store( + value = BlockchainProviderTypesConverter.convert(response), + ) + } + } + + private companion object { + const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}" + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt new file mode 100644 index 0000000000..9ede47a471 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -0,0 +1,43 @@ +package com.tangem.blockchainsdk + +import com.tangem.blockchain.common.AccountCreator +import com.tangem.blockchain.common.BlockchainFeatureToggles +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles +import timber.log.Timber +import javax.inject.Inject + +/** + * Creator of [WalletManagerFactory] + * + * @property accountCreator account creator + * @property blockchainDataStorage blockchain data storage + * @property blockchainSDKLogger blockchain SDK logger + * +[REDACTED_AUTHOR] + */ +internal class WalletManagerFactoryCreator @Inject constructor( + private val accountCreator: AccountCreator, + private val blockchainDataStorage: BlockchainDataStorage, + private val blockchainSDKLogger: BlockchainSDKLogger, + private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, +) { + + fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { + Timber.d("Create WalletManagerFactory") + + return WalletManagerFactory( + config = config, + blockchainProviderTypes = blockchainProviderTypes, + accountCreator = accountCreator, + featureToggles = BlockchainFeatureToggles( + isCardanoTokenSupport = blockchainSDKFeatureToggles.isCardanoTokensSupportEnabled, + ), + blockchainDataStorage = blockchainDataStorage, + loggers = listOf(blockchainSDKLogger), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultAccountCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt similarity index 80% rename from core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultAccountCreator.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt index 1501279d83..be9daa7448 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultAccountCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt @@ -1,14 +1,14 @@ -package com.tangem.datasource.local.blockchain +package com.tangem.blockchainsdk.accountcreator import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.toHexString +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody -import com.tangem.lib.auth.AuthProvider internal class DefaultAccountCreator( private val authProvider: AuthProvider, @@ -16,7 +16,10 @@ internal class DefaultAccountCreator( ) : AccountCreator { override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result { - val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString()) + val request = CreateUserNetworkAccountBody( + networkId = blockchain.id.removeSuffix("/test"), + walletPublicKey = walletPublicKey.toHexString(), + ) return try { val response = tangemTechApi.createUserNetworkAccount( cardPublicKey = authProvider.getCardPublicKey(), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt new file mode 100644 index 0000000000..f348d06b21 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.blockchainsdk.converters + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchainsdk.BlockchainProviderTypes +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.blockchainsdk.utils.createPrivateProviderType +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.datasource.config.models.ProviderModel +import com.tangem.utils.converter.Converter +import timber.log.Timber + +/** + * Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainProviderTypesConverter : + Converter { + + override fun convert(value: BlockchainProvidersResponse): BlockchainProviderTypes { + return value.mapNotNull { (networkId, blockchainProviders) -> + val blockchain = Blockchain.fromNetworkId(networkId) ?: return@mapNotNull null + + val providerTypes = blockchainProviders.mapNotNull { provider -> + when (provider) { + is ProviderModel.Public -> ProviderType.Public(url = provider.url) + is ProviderModel.Private -> createPrivateProviderType(name = provider.name) + ProviderModel.UnsupportedType -> { + Timber.e("$blockchain provider type is not supported") + null + } + } + } + + blockchain to providerTypes + } + .toMap() + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt new file mode 100644 index 0000000000..5d713313ea --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -0,0 +1,90 @@ +package com.tangem.blockchainsdk.converters + +import com.tangem.blockchain.common.* +import com.tangem.datasource.config.models.ConfigValueModel +import com.tangem.utils.converter.Converter + +/** + * Converts [ConfigValueModel] to [BlockchainSdkConfig] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainSDKConfigConverter : Converter { + + override fun convert(value: ConfigValueModel): BlockchainSdkConfig { + return BlockchainSdkConfig( + blockchairCredentials = BlockchairCredentials( + apiKey = value.blockchairApiKeys, + authToken = value.blockchairAuthorizationToken, + ), + blockcypherTokens = value.blockcypherTokens, + quickNodeSolanaCredentials = QuickNodeCredentials( + apiKey = value.quiknodeApiKey, + subdomain = value.quiknodeSubdomain, + ), + quickNodeBscCredentials = QuickNodeCredentials( + apiKey = value.bscQuiknodeApiKey, + subdomain = value.bscQuiknodeSubdomain, + ), + infuraProjectId = value.infuraProjectId, + tronGridApiKey = value.tronGridApiKey, + nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey), + getBlockCredentials = createGetBlockCredentials(value), + kaspaSecondaryApiUrl = value.kaspaSecondaryApiUrl, + tonCenterCredentials = TonCenterCredentials( + mainnetApiKey = value.tonCenterKeys.mainnet, + testnetApiKey = value.tonCenterKeys.testnet, + ), + chiaFireAcademyApiKey = value.chiaFireAcademyApiKey, + chiaTangemApiKey = value.chiaTangemApiKey, + hederaArkhiaApiKey = value.hederaArkhiaKey, + polygonScanApiKey = value.polygonScanApiKey, + bittensorDwellirApiKey = value.bittensorDwellirApiKey, + bittensorOnfinalityApiKey = value.bittensorOnfinalityKey, + ) + } + + private fun createGetBlockCredentials(configValues: ConfigValueModel): GetBlockCredentials? { + return configValues.getBlockAccessTokens?.let { accessTokens -> + GetBlockCredentials( + xrp = GetBlockAccessToken(jsonRpc = accessTokens.xrp?.jsonRPC), + cardano = GetBlockAccessToken(rosetta = accessTokens.cardano?.rosetta), + avalanche = GetBlockAccessToken(jsonRpc = accessTokens.avalanche?.jsonRPC), + eth = GetBlockAccessToken(jsonRpc = accessTokens.eth?.jsonRPC), + etc = GetBlockAccessToken(jsonRpc = accessTokens.etc?.jsonRPC), + fantom = GetBlockAccessToken(jsonRpc = accessTokens.fantom?.jsonRPC), + rsk = GetBlockAccessToken(jsonRpc = accessTokens.rsk?.jsonRPC), + bsc = GetBlockAccessToken(jsonRpc = accessTokens.bsc?.jsonRPC), + polygon = GetBlockAccessToken(jsonRpc = accessTokens.polygon?.jsonRPC), + gnosis = GetBlockAccessToken(jsonRpc = accessTokens.gnosis?.jsonRPC), + cronos = GetBlockAccessToken(jsonRpc = accessTokens.cronos?.jsonRPC), + solana = GetBlockAccessToken(jsonRpc = accessTokens.solana?.jsonRPC), + ton = GetBlockAccessToken(jsonRpc = accessTokens.ton?.jsonRPC), + tron = GetBlockAccessToken(rest = accessTokens.tron?.rest), + cosmos = GetBlockAccessToken(rest = accessTokens.cosmos?.rest), + near = GetBlockAccessToken(jsonRpc = accessTokens.near?.jsonRPC), + aptos = GetBlockAccessToken(rest = accessTokens.aptos?.rest), + dogecoin = GetBlockAccessToken( + jsonRpc = accessTokens.dogecoin?.jsonRPC, + blockBookRest = accessTokens.dogecoin?.blockBookRest, + ), + litecoin = GetBlockAccessToken( + jsonRpc = accessTokens.litecoin?.jsonRPC, + blockBookRest = accessTokens.litecoin?.blockBookRest, + ), + dash = GetBlockAccessToken( + jsonRpc = accessTokens.dash?.jsonRPC, + blockBookRest = accessTokens.dash?.blockBookRest, + ), + bitcoin = GetBlockAccessToken( + jsonRpc = accessTokens.bitcoin?.jsonRPC, + blockBookRest = accessTokens.bitcoin?.blockBookRest, + ), + algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), + zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC), + polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC), + base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC), + ) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultBlockchainDataStorage.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/datastorage/DefaultBlockchainDataStorage.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultBlockchainDataStorage.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/datastorage/DefaultBlockchainDataStorage.kt index 3f3d7192f5..85a3bab59f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultBlockchainDataStorage.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/datastorage/DefaultBlockchainDataStorage.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.blockchain +package com.tangem.blockchainsdk.datastorage import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt new file mode 100644 index 0000000000..7adb1648a3 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -0,0 +1,63 @@ +package com.tangem.blockchainsdk.di + +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.blockchainsdk.DefaultBlockchainSDKFactory +import com.tangem.blockchainsdk.WalletManagerFactoryCreator +import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator +import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage +import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles +import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.store.DefaultRuntimeStore +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.local.preferences.AppPreferencesStore +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 BlockchainSDKFactoryModule { + + @Provides + @Singleton + fun provideBlockchainSDKFactory( + assetLoader: AssetLoader, + blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + walletManagerFactoryCreator: WalletManagerFactoryCreator, + dispatchers: CoroutineDispatcherProvider, + ): BlockchainSDKFactory { + return DefaultBlockchainSDKFactory( + assetLoader = assetLoader, + blockchainProvidersResponseLoader = blockchainProvidersResponseLoader, + configStore = DefaultRuntimeStore(defaultValue = BlockchainSdkConfig()), + blockchainProviderTypesStore = DefaultRuntimeStore(defaultValue = emptyMap()), + walletManagerFactoryCreator = walletManagerFactoryCreator, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideWalletManagerFactoryCreator( + authProvider: AuthProvider, + tangemTechApi: TangemTechApi, + appPreferencesStore: AppPreferencesStore, + blockchainSDKLogger: BlockchainSDKLogger, + featureTogglesManager: FeatureTogglesManager, + ): WalletManagerFactoryCreator { + return WalletManagerFactoryCreator( + accountCreator = DefaultAccountCreator(authProvider, tangemTechApi), + blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), + blockchainSDKLogger = blockchainSDKLogger, + blockchainSDKFeatureToggles = DefaultBlockchainSDKFeatureToggles(featureTogglesManager), + ) + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt new file mode 100644 index 0000000000..fcdc2099b3 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.blockchainsdk.featuretoggles + +internal interface BlockchainSDKFeatureToggles { + + val isCardanoTokensSupportEnabled: Boolean +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt new file mode 100644 index 0000000000..ce7241d7d5 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.blockchainsdk.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultBlockchainSDKFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : BlockchainSDKFeatureToggles { + + override val isCardanoTokensSupportEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "CARDANO_TOKENS_SUPPORT_ENABLED") +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt new file mode 100644 index 0000000000..bfb8f6107d --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt @@ -0,0 +1,147 @@ +package com.tangem.blockchainsdk.loader + +import androidx.core.util.PatternsCompat +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.blockchainsdk.utils.createPrivateProviderType +import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.config.models.ProviderModel +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Loader of [BlockchainProvidersResponse] + * + * @property tangemTechServiceApi tangem tech api + * @property assetLoader asset loader for local config loading + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class BlockchainProvidersResponseLoader @Inject constructor( + private val tangemTechServiceApi: TangemTechServiceApi, + private val assetLoader: AssetLoader, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) + + /** Load [BlockchainProvidersResponse] */ + suspend fun load(): BlockchainProvidersResponse? { + val localResponse = loadLocal() ?: return null + + return runCatching(dispatcher = dispatchers.io, block = ::loadRemote) + .fold( + onSuccess = { remoteResponse -> mergeResponses(local = localResponse, remote = remoteResponse) }, + onFailure = { + Timber.e(it, "Failed to load blockchain provider types from backend") + localResponse + }, + ) + } + + private suspend fun loadLocal(): BlockchainProvidersResponse? { + return assetLoader.load(fileName = PROVIDER_TYPES_FILE_NAME) + } + + private suspend fun loadRemote() = tangemTechServiceApi.getBlockchainProviders() + + /** Merge blockchains with non-empty providers [remote] from remote with blockchains from local [local] */ + private fun mergeResponses( + local: BlockchainProvidersResponse, + remote: BlockchainProvidersResponse, + ): BlockchainProvidersResponse { + /* + * Example: + * val remote = mapOf("a" to 1, "b" to 2, "c" to 3) + * val local = mapOf("a" to 11, "e" to 4, "f" to 5) + * + * local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 } + */ + val remoteWithoutInvalidProviders = remote + .mapValues { + it.value + .filterUnsupportedProviders() + .filterInvalidProviders() + } + .filterValues { it.isNotEmpty() } + + val result = local + remoteWithoutInvalidProviders + + if (result != remote) { + val missingBlockchains = result.keys - remote.keys + val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys + + recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) + } + + return result.guaranteeUrlsEndWithSlash() + } + + private fun List.filterUnsupportedProviders() = filter { + val isSupportedType = it !is ProviderModel.UnsupportedType + + val isSupportedPrivateType = if (it is ProviderModel.Private) { + createPrivateProviderType(it.name) != null + } else { + true + } + + isSupportedType && isSupportedPrivateType + } + + private fun List.filterInvalidProviders() = mapNotNull { provider -> + if (provider is ProviderModel.Public) { + if (isValidUrl(provider.url)) provider else null + } else { + provider + } + } + + private fun isValidUrl(url: String): Boolean { + val forbiddenScheme = forbiddenSchemes.firstOrNull { url.startsWith(prefix = it) } + val inputUrl = if (forbiddenScheme != null) url.substringAfter(forbiddenScheme) else url + + return PatternsCompat.WEB_URL.matcher(inputUrl).matches() + } + + private fun recordException(missingBlockchains: Set) { + val exception = IllegalStateException( + "Remote config does not contain required blockchains or providers information: " + + missingBlockchains.joinToString(), + ) + + Timber.e(exception) + + firebaseCrashlytics.recordException(exception) + } + + /* + * Example: + * https://qwe.com --> https://qwe.com/ + */ + private fun BlockchainProvidersResponse.guaranteeUrlsEndWithSlash(): BlockchainProvidersResponse { + return mapValues { + it.value.map { provider -> provider.addSlashIfAbsent() } + } + } + + private fun ProviderModel.addSlashIfAbsent(): ProviderModel { + return if (this is ProviderModel.Public && url.last() != '/') { + copy(url = "$url/") + } else { + this + } + } + + private companion object { + const val PROVIDER_TYPES_FILE_NAME = "tangem-app-config/providers_order" + + val forbiddenSchemes = listOf("wss://") + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt new file mode 100644 index 0000000000..4278918e98 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt @@ -0,0 +1,20 @@ +package com.tangem.blockchainsdk.store + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Default implementation of RuntimeStore + * + * @param defaultValue default value + */ +internal class DefaultRuntimeStore(defaultValue: T) : RuntimeStore { + + private val flow = MutableStateFlow(value = defaultValue) + + override fun get(): StateFlow = flow + + override suspend fun store(value: T) { + flow.value = value + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt new file mode 100644 index 0000000000..2969e42576 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt @@ -0,0 +1,17 @@ +package com.tangem.blockchainsdk.store + +import kotlinx.coroutines.flow.StateFlow + +/** + * Runtime store + * +[REDACTED_AUTHOR] + */ +internal interface RuntimeStore { + + /** Get flow of elements [T] */ + fun get(): StateFlow + + /** Store [value] */ + suspend fun store(value: T) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt similarity index 94% rename from domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index b76e305758..8b2a9b6a39 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.common.extensions +package com.tangem.blockchainsdk.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token @@ -97,8 +97,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "zksync/test" -> Blockchain.ZkSyncEraTestnet "moonbeam" -> Blockchain.Moonbeam "moonbeam/test" -> Blockchain.MoonbeamTestnet - "manta-network" -> Blockchain.Manta - "manta-network/test" -> Blockchain.MantaTestnet + "manta-pacific" -> Blockchain.Manta + "manta-pacific/test" -> Blockchain.MantaTestnet "polygon-zkevm" -> Blockchain.PolygonZkEVM "polygon-zkevm/test" -> Blockchain.PolygonZkEVMTestnet "nexa" -> Blockchain.Nexa // FIXME @@ -114,6 +114,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "taraxa/test" -> Blockchain.TaraxaTestnet "base" -> Blockchain.Base "base/test" -> Blockchain.BaseTestnet + "koinos" -> Blockchain.Koinos + "koinos/test" -> Blockchain.KoinosTestnet + "joystream" -> Blockchain.Joystream + "bittensor" -> Blockchain.Bittensor else -> null } } @@ -212,8 +216,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.ZkSyncEraTestnet -> "zksync/test" Blockchain.Moonbeam -> "moonbeam" Blockchain.MoonbeamTestnet -> "moonbeam/test" - Blockchain.Manta -> "manta-network" - Blockchain.MantaTestnet -> "manta-network/test" + Blockchain.Manta -> "manta-pacific" + Blockchain.MantaTestnet -> "manta-pacific/test" Blockchain.PolygonZkEVM -> "polygon-zkevm" Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm/test" Blockchain.Nexa -> "nexa" // FIXME @@ -229,6 +233,10 @@ fun Blockchain.toNetworkId(): String { Blockchain.TaraxaTestnet -> "taraxa/test" Blockchain.Base -> "base" Blockchain.BaseTestnet -> "base/test" + Blockchain.Koinos -> "koinos" + Blockchain.KoinosTestnet -> "koinos/test" + Blockchain.Joystream -> "joystream" + Blockchain.Bittensor -> "bittensor" } } @@ -293,7 +301,7 @@ fun Blockchain.toCoinId(): String { Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain" Blockchain.ZkSyncEra, Blockchain.ZkSyncEraTestnet -> "zksync-ethereum" Blockchain.Moonbeam, Blockchain.MoonbeamTestnet -> "moonbeam" - Blockchain.Manta, Blockchain.MantaTestnet -> "manta-network-ethereum" + Blockchain.Manta, Blockchain.MantaTestnet -> "manta-pacific" Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum" Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME Blockchain.Radiant -> "radiant" @@ -302,6 +310,9 @@ fun Blockchain.toCoinId(): String { Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa" Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum" + Blockchain.Koinos, Blockchain.KoinosTestnet -> "koinos" + Blockchain.Joystream -> "joystream" + Blockchain.Bittensor -> "bittensor" } } @@ -330,8 +341,9 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, - Blockchain.Manta, - Blockchain.MantaTestnet, Blockchain.Mantle, Blockchain.MantleTestnet, + Blockchain.Koinos, + Blockchain.KoinosTestnet, + Blockchain.Bittensor, ) \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt new file mode 100644 index 0000000000..ba4a4fafd8 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt @@ -0,0 +1,32 @@ +package com.tangem.blockchainsdk.utils + +import com.tangem.blockchain.common.network.providers.ProviderType +import timber.log.Timber + +/** Create private provider by [name] or return null */ +@Suppress("CyclomaticComplexMethod") +fun createPrivateProviderType(name: String): ProviderType? { + return when (name) { + "blockchair" -> ProviderType.BitcoinLike.Blockchair + "blockcypher" -> ProviderType.BitcoinLike.Blockcypher + "adalite" -> ProviderType.Cardano.Adalite + "tangemRosetta" -> ProviderType.Cardano.Rosetta + "fireAcademy" -> ProviderType.Chia.FireAcademy + "tangemChia" -> ProviderType.Chia.Tangem + "infura" -> ProviderType.EthereumLike.Infura + "getblock" -> ProviderType.GetBlock + "arkhiaHedera" -> ProviderType.Hedera.Arkhia + "kaspa" -> ProviderType.Kaspa.SecondaryAPI + "nownodes" -> ProviderType.NowNodes + "quicknode" -> ProviderType.QuickNode + "solana" -> ProviderType.Solana.Official + "ton" -> ProviderType.Ton.TonCentral + "tron" -> ProviderType.Tron.TronGrid + "dwellirBittensor" -> ProviderType.Bittensor.Dwellir + "onfinalityBittensor" -> ProviderType.Bittensor.Onfinality + else -> { + Timber.e("Private provider with name $name is not supported") + null + } + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt new file mode 100644 index 0000000000..afbf3601ee --- /dev/null +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoaderTest.kt @@ -0,0 +1,293 @@ +package com.tangem.blockchainsdk.loader + +import android.annotation.SuppressLint +import com.google.common.truth.Truth +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.asset.reader.AssetReader +import com.tangem.datasource.config.models.ProviderModel +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +@SuppressLint("CheckResult") +@OptIn(ExperimentalStdlibApi::class) +internal class BlockchainProvidersResponseLoaderTest { + + private val tangemTechServiceApi = mockk() + private val assetReader = mockk() + private val moshi = mockk() + private val jsonAdapter = mockk>() + + // Impossible to mockk AssetLoader because it implement inline functions + private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi) + + private val loader = BlockchainProvidersResponseLoader( + tangemTechServiceApi = tangemTechServiceApi, + assetLoader = assetLoader, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val firebaseCrashlytics = mockk() + + @Before + fun setup() { + mockkStatic(FirebaseCrashlytics::class) + every { FirebaseCrashlytics.getInstance() } returns firebaseCrashlytics + } + + @Test + fun test_load_if_local_config_is_empty() = runTest { + val emptyJson = "" + everyGettingLocalConfig(json = emptyJson) returns null + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(emptyJson) + } + + coVerify(inverse = true) { tangemTechServiceApi.getBlockchainProviders() } + + Truth.assertThat(actual).isEqualTo(null) + } + + @Test + fun test_load_if_remote_config_loading_is_loaded_failure() = runTest { + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProviders + coEvery { tangemTechServiceApi.getBlockchainProviders() } throws IllegalStateException("Test exception") + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + } + + Truth.assertThat(actual).isEqualTo(localProviders) + } + + @Test + fun test_load_if_remote_config_loading_is_loaded_successful() = runTest { + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProviders + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + } + + Truth.assertThat(actual).isEqualTo(remoteProviders) + } + + @Test + fun test_load_if_remote_config_is_the_same_as_local() = runTest { + val remoteProviders = localProviders + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProviders + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + } + + Truth.assertThat(actual).isEqualTo(remoteProviders) + } + + @Test + fun test_load_if_remote_config_has_empty_providers() = runTest { + val ethProvider = "ethereum" to emptyList() + val remoteProvidersWithEth = remoteProviders + ethProvider + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProviders + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth + everyCrashlyticsRecording() just Runs + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + firebaseCrashlytics.recordException(any()) + } + + Truth.assertThat(actual).isEqualTo(remoteProviders) + } + + @Test + fun test_load_if_local_config_has_empty_providers() = runTest { + val localProvidersWithEmptyApt = localProviders.mapValues { if (it.key == "aptos") emptyList() else it.value } + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProvidersWithEmptyApt + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProviders + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + } + + val expected = localProvidersWithEmptyApt + remoteProviders + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_load_if_remote_config_doesnt_contain_local_providers() = runTest { + val remoteProvidersWithoutLocal: BlockchainProvidersResponse = remoteProviders - localProviders.keys + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProviders + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithoutLocal + everyCrashlyticsRecording() just Runs + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + firebaseCrashlytics.recordException(any()) + } + + Truth.assertThat(actual).isEqualTo(remoteProviders) + } + + @Test + fun test_load_if_remote_config_contains_unsupported_types() = runTest { + val ethProvider = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Private(name = "nownodes")) + val remoteProvidersWithEth = remoteProviders + ethProvider + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProviders + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth + everyCrashlyticsRecording() just Runs + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + firebaseCrashlytics.recordException(any()) + } + + val expected = remoteProviders + ("ethereum" to listOf(ProviderModel.Private(name = "nownodes"))) + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_load_if_remote_config_contains_invalid_public_providers() = runTest { + val localEthProvider = "ethereum" to listOf(ProviderModel.Private(name = "nownodes")) + val localProvidersWithEth = localProviders + localEthProvider + + val remoteEthProvider = "ethereum" to listOf(ProviderModel.UnsupportedType, ProviderModel.Public("adbw2138")) + val remoteProvidersWithEth = remoteProviders + remoteEthProvider + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProvidersWithEth + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithEth + everyCrashlyticsRecording() just Runs + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + firebaseCrashlytics.recordException(any()) + } + + val expected = remoteProviders + localEthProvider + + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun test_load_if_configs_contain_public_providers_without_slash_in_the_end() = runTest { + val localPublicProviderUrl = "https://qwe.com" + val localEthProvider = "ethereum" to listOf(ProviderModel.Public(url = localPublicProviderUrl)) + val localProvidersWithEth = localProviders + localEthProvider + + val remotePublicProviderUrl = "https://rty.com" + val remoteDogeProvider = "dogecoin" to listOf(ProviderModel.Public(url = remotePublicProviderUrl)) + val remoteProvidersWithDoge = remoteProviders + remoteDogeProvider + + everyGettingLocalConfig(json = LOCAL_PROVIDERS_JSON) returns localProvidersWithEth + coEvery { tangemTechServiceApi.getBlockchainProviders() } returns remoteProvidersWithDoge + everyCrashlyticsRecording() just Runs + + val actual = loader.load() + + coVerifyOrder { + assetReader.read(LOCAL_CONFIG_FILE_NAME) + moshi.adapter() + jsonAdapter.fromJson(LOCAL_PROVIDERS_JSON) + tangemTechServiceApi.getBlockchainProviders() + firebaseCrashlytics.recordException(any()) + } + + val expected = remoteProviders + + localEthProvider.copy(second = listOf(ProviderModel.Public(url = "$localPublicProviderUrl/"))) + + remoteDogeProvider.copy(second = listOf(ProviderModel.Public(url = "$remotePublicProviderUrl/"))) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun everyGettingLocalConfig( + json: String, + ): MockKStubScope { + coEvery { assetReader.read(LOCAL_CONFIG_FILE_NAME) } returns json + every { moshi.adapter() } returns jsonAdapter + return coEvery { jsonAdapter.fromJson(json) } + } + + private fun everyCrashlyticsRecording() = every { firebaseCrashlytics.recordException(any()) } + + private companion object { + const val LOCAL_CONFIG_FILE_NAME = "tangem-app-config/providers_order.json" + + const val LOCAL_PROVIDERS_JSON = "doesn't matter" + + val localProviders: BlockchainProvidersResponse = mapOf( + "aptos" to listOf(ProviderModel.Private(name = "nownodes")), + "algorand" to listOf( + ProviderModel.Private(name = "nownodes"), + ProviderModel.Public(url = "https://public_alg.com/"), + ), + ) + + val remoteProviders: BlockchainProvidersResponse = localProviders + mapOf( + "bitcoin" to listOf(ProviderModel.Private(name = "blockchair")), + ) + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 85ef064f98..35b8386cd9 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -45,6 +45,11 @@ object BlockchainUtils { return blockchain == Blockchain.Tezos } + fun isCardano(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Cardano + } + /** If current [networkId] is BeaconChain */ fun isBeaconChain(networkId: String): Boolean { val blockchain = Blockchain.fromId(networkId) diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index c2e37da872..9bdde82e08 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -1,6 +1,5 @@ package com.tangem.lib.crypto -import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.ProxyAmount /** @@ -8,28 +7,11 @@ import com.tangem.lib.crypto.models.ProxyAmount */ interface UserWalletManager { - /** - * Returns all user tokens (merged from local and backend) - */ - @Throws(IllegalStateException::class) - suspend fun getUserTokens(networkId: String, derivationPath: String?, isExcludeCustom: Boolean): List - - @Throws(IllegalStateException::class) - fun getNativeTokenForNetwork(networkId: String): Currency - /** * Returns user walletId or empty string */ fun getWalletId(): String - /** - * Checks that token added to user wallet - * - * @param currency to receive referral payments - */ - @Throws(IllegalStateException::class) - suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean - suspend fun hideAllTokens() /** @@ -41,21 +23,6 @@ interface UserWalletManager { @Throws(IllegalStateException::class) suspend fun getWalletAddress(networkId: String, derivationPath: String?): String - /** - * Return balances from wallet found by networkId - * - * @param networkId - * @param extraTokens tokens you want to check balance that not exists in wallet - * @param derivationPath if null uses default - * @return map of - */ - @Throws(IllegalStateException::class) - suspend fun getCurrentWalletTokensBalance( - networkId: String, - extraTokens: List, - derivationPath: String?, - ): Map - @Throws(IllegalStateException::class) suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt index 56b92d5688..a4643460ea 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt @@ -1,8 +1,21 @@ package com.tangem.lib.crypto.models +import java.math.BigDecimal import java.math.BigInteger -data class ProxyFee( - val gasLimit: BigInteger, - val fee: ProxyAmount, -) \ No newline at end of file +sealed interface ProxyFee { + + val gasLimit: BigInteger + val fee: ProxyAmount + + data class Common( + override val gasLimit: BigInteger, + override val fee: ProxyAmount, + ) : ProxyFee + + data class CardanoToken( + override val gasLimit: BigInteger, + override val fee: ProxyAmount, + val minAdaValue: BigDecimal, + ) : ProxyFee +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index f288d89a7d..885f32c1d7 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -37,7 +37,7 @@ private fun AppExtension.configureDefaultConfig(project: Project) { buildFeatures.buildConfig = true - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.tangem.common.HiltTestRunner" } } @@ -76,6 +76,7 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil } BuildType.Internal, BuildType.External, + BuildType.Mocked -> { initWith(extension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt index 66437a1de5..17f61ecdd2 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -15,6 +15,7 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v value = "\"$value\"", ) + // TODO remove class TestActionEnabled(isEnabled: Boolean) : BuildConfigField( type = "Boolean", name = "TEST_ACTION_ENABLED", @@ -32,4 +33,10 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v name = "TESTER_MENU_ENABLED", value = isEnabled.toString(), ) + + class MockDataSource(isEnabled: Boolean) : BuildConfigField( + type = "Boolean", + name = "MOCK_DATA_SOURCE", + value = isEnabled.toString(), + ) } \ No newline at end of file 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 767e46ba25..53a1c18637 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 @@ -27,6 +27,29 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), + ), + ), + + /** + * Build type for QA and business + * + * Features: + * - Env: dev + * - Signing config: debug + * - Logs + * - Enabled mocked datasource + * */ + Mocked( + id = "mocked", + appIdSuffix = "mocked", + versionSuffix = "mocked", + configFields = listOf( + BuildConfigField.Environment(value = "dev"), + BuildConfigField.TestActionEnabled(isEnabled = false), + BuildConfigField.LogEnabled(isEnabled = true), + BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = true), ), ), @@ -50,6 +73,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), ), ), @@ -70,6 +94,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), ), ), @@ -88,6 +113,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), ), ), } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f16688065c..9f9643c29f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,10 +24,41 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - google() + google { + content { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() - mavenLocal() - jcenter() // unable to replace with mavenCentral() due to rekotlin + mavenLocal { + content { + includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeModule("com.tangem", "blstlib") + includeModule("com.tangem", "blockchain") + includeModule("com.tangem", "wallet-core-proto") + includeModule("com.tangem", "wallet-core") + } + } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/tangem-sdk-android") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") } + } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/blst-android") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { includeModule("com.tangem", "blstlib") } + } maven { // setting any repository from tangem project allows maven search all packages in the project url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") @@ -35,6 +66,7 @@ dependencyResolutionManagement { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } + content { includeModule("com.tangem", "blockchain") } } maven { // setting any repository from tangem project allows maven search all packages in the project @@ -43,6 +75,15 @@ dependencyResolutionManagement { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } + content { + includeModule("com.tangem", "wallet-core-proto") + includeModule("com.tangem", "wallet-core") + } + } + jcenter { // unable to replace with mavenCentral() due to rekotlin + content { + includeModule("org.rekotlin", "rekotlin") + } } maven("https://jitpack.io") maven("https://clients-nexus.sprinklr.com/") @@ -71,11 +112,13 @@ include(":core:ui") include(":core:utils") include(":core:deep-links") include(":core:deep-links:global") +include(":core:decompose") // endregion Core modules // region Libs modules -include(":libs:crypto") include(":libs:auth") +include(":libs:blockchain-sdk") +include(":libs:crypto") include(":libs:visa") // endregion Libs modules @@ -110,6 +153,12 @@ include(":features:manage-tokens:impl") include(":features:qr-scanning:api") include(":features:qr-scanning:impl") + +include(":features:staking:api") +include(":features:staking:impl") + +include(":features:details:api") +include(":features:details:impl") // endregion Feature modules // region Domain modules @@ -134,12 +183,15 @@ include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") +include(":domain:transaction:models") include(":domain:analytics") include(":domain:visa") include(":domain:onboarding") include(":domain:feedback") include(":domain:qr-scanning") include(":domain:qr-scanning:models") +include(":domain:staking") +include(":domain:wallet-connect") // endregion Domain modules // region Data modules @@ -149,7 +201,6 @@ include(":data:balance-hiding") include(":data:common") include(":data:card") include(":data:tokens") -include(":data:source:preferences") include(":data:settings") include(":data:txhistory") include(":data:wallets") @@ -160,4 +211,6 @@ include(":data:promo") include(":data:onboarding") include(":data:feedback") include(":data:qr-scanning") +include(":data:staking") +include(":data:wallet-connect") // endregion Data modules \ No newline at end of file