diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3682efb9f5..f775c46b33 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) @@ -80,6 +80,7 @@ dependencies { 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 +88,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) @@ -183,7 +183,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 +215,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 +166,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 +216,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() @@ -264,9 +267,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 } } @@ -303,7 +306,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()) } @@ -437,15 +440,10 @@ 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 + val canSaveWallets = runCatching { userWalletsListManager.asLockable()?.isLockedSync } + .fold(onSuccess = { true }, onFailure = { false }) - if (canSaveWallets && hasSavedWallets) { + if (canSaveWallets && userWalletsListManager.hasUserWallets) { store.dispatch( NavigationAction.NavigateTo( screen = AppScreen.Welcome, diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt similarity index 70% rename from app/src/main/java/com/tangem/tap/TapApplication.kt rename to app/src/main/java/com/tangem/tap/TangemApplication.kt index 7c26fe7570..5a22b825cb 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.reader.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config @@ -37,18 +34,17 @@ import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.FeedbackManagerFeatureToggles 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.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 +57,123 @@ 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 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 + 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 assetReader: AssetReader + get() = entryPoint.getAssetReader() - @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 cardRepository: CardRepository + get() = entryPoint.getCardRepository() - @Inject - lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + get() = entryPoint.getFeedbackManagerFeatureToggles() - @Inject - lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase + private val tangemSdkLogger: TangemSdkLogger + get() = entryPoint.getTangemSdkLogger() - @Inject - lateinit var cardRepository: CardRepository + private val settingsRepository: SettingsRepository + get() = entryPoint.getSettingsRepository() - @Inject - lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles - - @Inject - lateinit var blockchainSDKLogger: BlockchainSDKLogger - - @Inject - lateinit var tangemSdkLogger: TangemSdkLogger - // endregion Injected + private val blockchainSDKFactory: BlockchainSDKFactory + get() = entryPoint.getBlockchainSDKFactory() override fun onCreate() { super.onCreate() + init() + } + + fun init() { store = createReduxStore() if (BuildConfig.LOG_ENABLED) { @@ -206,19 +191,10 @@ 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() - } } val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) @@ -264,16 +240,14 @@ internal class TapApplication : Application(), ImageLoaderFactory { balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, sendFeatureToggles = sendFeatureToggles, - blockchainDataStorage = blockchainDataStorage, - accountCreator = accountCreator, - userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles, generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, cardRepository = cardRepository, feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, tangemSdkLogger = tangemSdkLogger, - blockchainSDKLogger = blockchainSDKLogger, + settingsRepository = settingsRepository, + blockchainSDKFactory = blockchainSDKFactory, ), ), ) @@ -305,7 +279,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) @@ -373,14 +346,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/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/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/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/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/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..4fb2375cf9 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 @@ -9,7 +9,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 @@ -101,6 +100,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..b50d350a28 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 { @@ -107,6 +101,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) { scope.launch { val scanResponseProvider: () -> ScanResponse? = { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.selectedUserWalletSync?.scanResponse } val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card } @@ -148,33 +143,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..7a386bdfd1 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,18 @@ 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.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 +23,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( 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/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/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/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 084134986d..4434311a95 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,7 +6,7 @@ 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 dagger.Module import dagger.Provides @@ -34,14 +34,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 { @@ -66,8 +58,8 @@ internal object CardDomainModule { @Provides @ViewModelScoped - fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase(walletStateHolder) + fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) } @Provides 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 3c9fb7b2a9..da46031149 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,7 +9,7 @@ import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SwapPromoRepository -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 @@ -140,4 +140,20 @@ internal object SettingsDomainModule { fun provideNeverShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverShowTapHelpUseCase { return NeverShowTapHelpUseCase(settingsRepository = settingsRepository) } + + @Provides + @ViewModelScoped + fun provideSetSaveWalletScreenShownUseCase( + settingsRepository: SettingsRepository, + ): SetSaveWalletScreenShownUseCase { + return SetSaveWalletScreenShownUseCase(settingsRepository = settingsRepository) + } + + @Provides + @ViewModelScoped + 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/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 81d84db2d2..6821d4a992 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,7 +6,6 @@ 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 @@ -55,9 +54,8 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetTokenListUseCase { - return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository) } @Provides @@ -66,9 +64,8 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetCardTokensListUseCase { - return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository) } @Provides @@ -192,7 +189,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - sendFeatureToggles: SendFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -201,7 +197,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - sendFeatureToggles = sendFeatureToggles, dispatchers = dispatchers, ) } 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..2bd8c1d4c1 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,13 @@ 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.blockchainsdk.BlockchainSDKFactory +import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.SdkMoshi 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 dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -28,26 +23,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, + blockchainSDKFactory: BlockchainSDKFactory, ): WalletManagersFacade { return DefaultWalletManagersFacade( walletManagersStore = walletManagersStore, userWalletsStore = userWalletsStore, - configManager = configManager, - blockchainDataStorage = blockchainDataStorage, assetReader = assetReader, moshi = moshi, - mnemonic = mnemonicRepository.generateDefaultMnemonic(), - accountCreator = accountCreator, - blockchainSDKLogger = blockchainSDKLogger, - feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + 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..de5aa6d294 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,7 +2,7 @@ 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.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletAddressServiceRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* @@ -19,32 +19,34 @@ internal object WalletsDomainModule { @Provides @ViewModelScoped - fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { - return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { + return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase { - return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { + return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesGetSelectedWalletSyncUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetSelectedWalletSyncUseCase( + userWalletsListManager: UserWalletsListManager, + ): GetSelectedWalletSyncUseCase { + return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { - return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { + return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @@ -55,29 +57,29 @@ internal object WalletsDomainModule { @Provides @ViewModelScoped - fun providesUnlockWalletUseCase(walletsStateHolder: WalletsStateHolder): UnlockWalletsUseCase { - return UnlockWalletsUseCase(walletsStateHolder = walletsStateHolder) + fun providesUnlockWalletUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase { + return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped 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) + fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { + return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase { - return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { + return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides 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..14f5f5e725 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 @@ -12,14 +10,11 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet 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 -import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider @@ -32,29 +27,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. @@ -79,9 +57,7 @@ class TapWalletManager( // 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/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 84d55370ed..d5fe11422f 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 @@ -13,7 +13,7 @@ import com.tangem.domain.userwallets.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/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..7a576f77b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -0,0 +1,128 @@ +package com.tangem.tap.domain.sdk + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.tangem.Message +import com.tangem.common.* +import com.tangem.common.authentication.keystore.KeystoreManager +import com.tangem.common.core.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.secure.SecureStorage +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +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 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 71% 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..3955158fd9 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 @@ -28,11 +28,16 @@ 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.sdk.TangemSdkManager 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.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 +45,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 +60,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 +92,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 +108,7 @@ class TangemSdkManager( ) } - suspend fun importWallet( + override suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, passphrase: String?, @@ -135,14 +140,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 +158,7 @@ class TangemSdkManager( ) } - suspend fun resetToFactorySettings( + override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { @@ -167,7 +172,7 @@ class TangemSdkManager( .map { CardDTO(it) } } - suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { return userCodeRepository.save( cardsIds = cardsIds, userCode = UserCode( @@ -177,15 +182,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 +198,7 @@ class TangemSdkManager( ) } - suspend fun setAccessCode(cardId: String?): CompletionResult { + override suspend fun setAccessCode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, @@ -201,7 +206,7 @@ class TangemSdkManager( ) } - suspend fun setLongTap(cardId: String?): CompletionResult { + override suspend fun setLongTap(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, @@ -209,7 +214,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 +225,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 +237,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 +261,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 +270,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..e690748bd5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -0,0 +1,175 @@ +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.* +import com.tangem.common.authentication.keystore.DummyKeystoreManager +import com.tangem.common.core.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.InMemoryStorage +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +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 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/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index d229d8f05d..cb4b7918e9 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,11 +3,11 @@ 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.models.UserWalletId 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..34b7dada02 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 @@ -9,7 +9,7 @@ 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.datasource.asset.reader.AssetReader import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.wallet.CreateWalletResponse @@ -27,8 +27,7 @@ class TwinCardsManager( private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString()) suspend fun createFirstWallet(message: Message): CompletionResult { - val response = tangemSdkManager.runTaskAsync( - runnable = CreateFirstTwinWalletTask(firstCardId), + val response = tangemSdkManager.createFirstTwinWallet( cardId = firstCardId, initialMessage = message, ) @@ -44,14 +43,15 @@ class TwinCardsManager( preparingMessage: Message, creatingWalletMessage: Message, ): CompletionResult { - val task = CreateSecondTwinWalletTask( + val response = tangemSdkManager.createSecondTwinWallet( firstPublicKey = currentCardPublicKey!!, firstCardId = firstCardId, issuerKeys = issuerKeyPair, preparingMessage = preparingMessage, creatingWalletMessage = creatingWalletMessage, + initialMessage = initialMessage, ) - val response = tangemSdkManager.runTaskAsync(task, null, initialMessage) + when (response) { is CompletionResult.Success -> { secondCardPublicKey = response.data.wallet.publicKey.toHexString() @@ -62,8 +62,9 @@ class TwinCardsManager( } suspend fun complete(message: Message): Result { - val response = tangemSdkManager.runTaskAsync( - runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair), + val response = tangemSdkManager.finalizeTwin( + secondCardPublicKey = secondCardPublicKey!!.hexToBytes(), + issuerKeyPair = issuerKeyPair, cardId = firstCardId, initialMessage = message, ) @@ -88,6 +89,7 @@ class TwinCardsManager( ) } + @Deprecated(message = "Use AssetReader instead") private fun getIssuers(reader: AssetReader): List { val file = reader.readJson(fileName = "tangem-app-config/issuers") return getAdapter().fromJson(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/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..cd502dc24e 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,6 +43,10 @@ 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? { val transaction = data.transaction @@ -227,11 +228,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/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 927eab61da..faaa951d2e 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,6 +6,9 @@ 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 @@ -35,6 +38,9 @@ internal object WalletConnectInteractorModule { wcRepository: WalletConnectRepository, 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(), ) } } 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..e2e43d4975 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 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() } @@ -258,6 +321,38 @@ class WalletConnectInteractor( return uri.lowercase().startsWith(WC_SCHEME) } + 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): WcPreparedRequest? { return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) } 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/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 3dd2804b29..5a4cafa1bb 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 @@ -13,10 +13,15 @@ import androidx.lifecycle.viewModelScope 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 @@ -232,7 +237,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 +321,7 @@ internal class AddCustomTokenViewModel @Inject constructor( type = DerivationPathSelectorType.CUSTOM, derivationPath = "", ), - ) + Blockchain.values() + ) + Blockchain.entries .filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() } 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/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 654edd4102..edef362685 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 @@ -18,7 +17,6 @@ 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.legacy.asLockable import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -27,8 +25,6 @@ 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 @@ -83,6 +79,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 @@ -329,8 +327,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 +368,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 +385,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 +395,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 +412,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 { @@ -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, @@ -610,6 +525,7 @@ class DetailsMiddleware { val userWallet = UserWalletBuilder(scanResponse).build() ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) return userWalletsListManager.save(userWallet) .doOnSuccess { store.onUserWalletSelected(userWallet) 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..117b13438c 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 @@ -7,7 +7,6 @@ import com.tangem.domain.models.scan.CardDTO 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 @@ -75,7 +74,9 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta 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() 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..b480ea809c 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,25 @@ 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.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.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 kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -46,7 +27,6 @@ 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 @@ -66,40 +46,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 +70,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 +79,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() @@ -397,140 +173,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/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index d050b873e5..56880350cc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder 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.features.details.redux.DetailsState @@ -36,6 +37,9 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber = mutableStateOf(updateState(store.state.detailsState)) 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..8528a2ebe0 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 @@ -11,7 +11,6 @@ import com.tangem.core.navigation.NavigationAction 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 @@ -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/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index bd3f2a5dde..844d5e6663 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,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.tap.common.compose.resources.C +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 +57,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, 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..3a9ebaf1bf 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,7 @@ 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.test.TestTags import com.tangem.wallet.R @Composable @@ -33,7 +33,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 +41,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, ) } 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..862e4ccb8a 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 @@ -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 @@ -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( @@ -108,6 +108,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { return@launch } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -127,6 +128,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/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index 23f8ec2235..46f7d2d07d 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 @@ -20,7 +19,7 @@ class WalletConnectLinkIntentHandler : IntentHandler { 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 6f5d289dd9..6c4c24f485 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 @@ -11,11 +14,17 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase 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.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") @@ -26,6 +35,10 @@ 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 dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -39,7 +52,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() observeFlips() displayBalancesHidingStatusToast() @@ -50,6 +70,38 @@ 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()) + + 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/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 050f08d79e..0c6c5601a2 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 @@ -14,12 +14,15 @@ 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.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) @@ -151,6 +151,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..5a12a5ea5e 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,7 @@ 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.datasource.asset.reader.AssetReader import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager 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..a1c84cb01a 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 @@ -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) { @@ -244,8 +245,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 +366,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..ca8d61618e 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,7 +15,7 @@ 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.datasource.asset.reader.AssetReader import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.Artwork 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..3a6b93da1a 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,7 +11,6 @@ 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 @@ -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), + ), + ) + } + } + } } } } @@ -551,6 +556,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 +568,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..bdfffe8f9f 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 @@ -7,9 +7,7 @@ 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.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 @@ -75,6 +73,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 +90,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 +108,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 +124,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 +144,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/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/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index e85db98c7b..519637bd2f 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 @@ -12,13 +12,13 @@ import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactio import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult +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.models.scan.CardDTO import com.tangem.domain.tokens.legacy.TradeCryptoAction 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..f9390fc4a2 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.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/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..4956441790 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 @@ -16,13 +16,16 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.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 @@ -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") @@ -115,6 +119,7 @@ internal class WelcomeMiddleware { scanCardInternal { scanResponse -> val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -140,12 +145,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 +160,7 @@ internal class WelcomeMiddleware { } private suspend fun disableUserWalletsSaving() { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.clear() .flatMap { tangemSdkManager.clearSavedUserCodes() } .doOnFailure { e -> @@ -165,9 +173,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/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/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/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 0185472a6e..06b2edda3a 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,9 +1,9 @@ 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.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAuthProvider 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 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..934f519967 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -21,17 +21,17 @@ 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( 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/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index c23def20bc..9001b33eea 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,7 @@ 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.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -15,11 +13,11 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles 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.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles @@ -61,14 +59,12 @@ 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 cardRepository: CardRepository? = null, val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null, val tangemSdkLogger: TangemSdkLogger? = null, - val blockchainSDKLogger: BlockchainSDKLogger? = null, + val settingsRepository: SettingsRepository? = null, + val blockchainSDKFactory: BlockchainSDKFactory? = 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/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/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index e4239e40d6..26d30378a2 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,14 @@ 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) } \ 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/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b7055574a3..76c7dce8bd 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 @@ -42,6 +42,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, 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 63e632438e..0a5dcf3fd9 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 @@ -76,10 +76,54 @@ 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, @Header("card_id") cardId: String, @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 } \ 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..f5a24561f8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.api.tangemTech + +import com.tangem.datasource.config.models.ProviderModel +import retrofit2.http.GET +import retrofit2.http.Header + +/** + * Tangem Tech API for app services + * +[REDACTED_AUTHOR] + */ +interface TangemTechServiceApi { + + @GET("networks/providers") + suspend fun getBlockchainProviders( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + ): 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/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt new file mode 100644 index 0000000000..042319e1f4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -0,0 +1,87 @@ +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 com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject + +/** + * Asset file loader + * +[REDACTED_AUTHOR] + */ +class AssetLoader @Inject constructor( + val assetReader: AssetReader, + @NetworkMoshi val moshi: Moshi, + val dispatchers: CoroutineDispatcherProvider, +) { + + /** Load content [Content] of asset file [fileName] */ + @OptIn(ExperimentalStdlibApi::class) + suspend inline fun load(fileName: String): Content? { + return runCatching(dispatchers.io) { + val json = assetReader.readJson(fileName = fileName) + + 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(dispatchers.io) { + val json = assetReader.readJson(fileName = fileName) + + 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(dispatchers.io) { + val json = assetReader.readJson(fileName = fileName) + + 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/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt similarity index 79% rename from core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt rename to core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt index ce19ff204e..4a8678e96d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.asset +package com.tangem.datasource.asset.reader import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext @@ -21,8 +21,7 @@ internal class AndroidAssetReader @Inject constructor( .use(BufferedReader::readText) } - override fun openFile(fileName: String): InputStream { - return context.assets - .open(fileName) + override fun openFile(file: String): InputStream { + return context.assets.open(file) } } \ 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/reader/AssetReader.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt rename to core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt index a3196c64aa..46c3edea4a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.asset +package com.tangem.datasource.asset.reader import java.io.InputStream 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..f7565b73c2 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 @@ -100,7 +100,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { chiaFireAcademyApiKey = configValues.chiaFireAcademyApiKey, chiaTangemApiKey = configValues.chiaTangemApiKey, ), - appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, sprinklr = configValues.sprinklr, walletConnectProjectId = configValues.walletConnectProjectId, @@ -146,8 +145,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..5c162f28a8 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 @@ -2,7 +2,7 @@ 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.reader.AssetReader import com.tangem.datasource.config.models.ConfigModel import com.tangem.datasource.config.models.ConfigValueModel import com.tangem.datasource.config.models.FeatureModel @@ -11,6 +11,7 @@ import timber.log.Timber /** [REDACTED_AUTHOR] */ +@Deprecated(message = "Use AssetReader instead") class FeaturesLocalLoader( private val assetReader: AssetReader, private val moshi: Moshi, 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..28e5b2b4d3 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, 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..5d2560be9e 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, @@ -80,11 +80,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 index 7df5502310..b39cb4f1a3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.di -import com.tangem.datasource.asset.AndroidAssetReader -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.reader.AndroidAssetReader +import com.tangem.datasource.asset.reader.AssetReader import dagger.Binds import dagger.Module import dagger.hilt.InstallIn 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..3884c6c9a5 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,13 @@ 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.config.models.ProviderModel import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,6 +23,12 @@ 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()) 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..e687c394e9 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 @@ -6,6 +6,8 @@ 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.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.addHeaders import com.tangem.datasource.utils.addLoggers @@ -19,6 +21,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 @@ -55,53 +58,94 @@ class NetworkModule { @Provides @Singleton - fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(PROD_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(), - ) - .build() - .create(TangemTechApi::class.java) + 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, + context, + appVersionProvider, + PROD_V1_TANGEM_TECH_BASE_URL, + timeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, + ) + } + + private inline fun provideTangemTechApiInternal( + moshi: Moshi, + context: Context, + appVersionProvider: AppVersionProvider, + baseUrl: String, + timeoutSeconds: Long? = null, + ): T { + val client = OkHttpClient.Builder() + .let { builder -> + if (timeoutSeconds != null) { + builder.callTimeout(timeoutSeconds, TimeUnit.SECONDS) + } else { + builder + } + } + .addHeaders( + CacheControlHeader, + AppVersionPlatformHeaders(appVersionProvider), + // 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 PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/" 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 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 const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/" const val API_ONE_INCH_TIMEOUT_MS = 5000L 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..bbd6ac04ad 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 @@ -2,7 +2,7 @@ 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.reader.AssetReader import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage import com.tangem.datasource.local.testnet.TestnetTokensStorage import dagger.Module 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 bec274ae30..4dfd9d6966 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") } @@ -75,6 +81,12 @@ 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") } + fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") } @@ -82,6 +94,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, @@ -89,6 +102,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..878702cd4f 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,7 +1,7 @@ package com.tangem.datasource.local.testnet import com.squareup.moshi.JsonAdapter -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.local.testnet.models.TestnetTokensConfig /** @@ -17,6 +17,7 @@ internal class DefaultTestnetTokensStorage( private val adapter: JsonAdapter, ) : TestnetTokensStorage { + @Deprecated(message = "Use AssetReader instead") override fun getConfig(): TestnetTokensConfig { return requireNotNull( value = adapter.fromJson( 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..1ee0e4d9f9 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,8 +1,7 @@ package com.tangem.datasource.utils +import com.tangem.datasource.api.common.AuthProvider import com.tangem.lib.auth.AppVersionProvider -import com.tangem.lib.auth.AuthBearerProvider -import com.tangem.lib.auth.AuthProvider import com.tangem.lib.auth.ExpressAuthProvider /** @@ -15,7 +14,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,10 +27,6 @@ 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() }, "platform" to { "android" }, 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..7dfba4d379 --- /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 { + implementation(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/model/Model.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt new file mode 100644 index 0000000000..b7a6a28adb --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/Model.kt @@ -0,0 +1,36 @@ +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.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +/** + * 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() } + } +} \ 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/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index cf1fb0dc1d..7de6a6f134 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -11,10 +11,6 @@ "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "5.9.1" }, - { - "name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED", - "version": "5.8.0" - }, { "name": "LOCAL_USER_LOGS_ENABLED", "version": "5.10.0" @@ -26,5 +22,9 @@ { "name": "WC_SOLANA_TX_SIGN_ENABLED", "version": "5.11.0" + }, + { + "name": "TOKEN_LIST_LCE_ENABLED", + "version": "5.10.0" } ] 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..c5bc28999b 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 @@ -10,7 +10,7 @@ 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.reader.AssetReader import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module import dagger.Provides 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..ad0f4f9939 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 @@ -3,7 +3,7 @@ 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 com.tangem.datasource.asset.reader.AssetReader import timber.log.Timber import kotlin.properties.Delegates @@ -24,6 +24,7 @@ internal class LocalFeatureTogglesStorage( override var featureToggles: List by Delegates.notNull() private set + @Deprecated(message = "Use AssetReader instead") override suspend fun init() { runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) } .onSuccess { featureToggles = it } 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..0f42883777 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 com.tangem.datasource.asset.reader.AssetReader import io.mockk.coEvery import io.mockk.mockk import io.mockk.verifyAll import io.mockk.verifyOrder -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import java.io.IOException @@ -16,7 +15,6 @@ import java.io.IOException /** [REDACTED_AUTHOR] */ -@OptIn(ExperimentalCoroutinesApi::class) @SuppressLint("CheckResult") internal class LocalFeatureTogglesStorageTest { diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index f12dc6ee9c..6e9e7bf51e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -553,10 +553,12 @@ Балансы показаны Отменить Выбранная операция в данный момент недоступна. Попробуйте позже. - В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением. - У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. - Обмен %s не доступен. Но мы работаем над его добавлением. - В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением. + В данный момент покупка монеты %s недоступна. Следите за нашими обновлениями. + У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. + У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. + В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. + Продажа средств станет доступной после завершения транзакции %s + В данный момент продажа %s недоступна. Следите за нашими обновлениями. Выберите адрес Сгенерировать XPUB Скрыть @@ -668,9 +670,10 @@ Возможно, данная карта - образец или подделка Ошибка проверки подлинности Ассоциировать - Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%.4f %s + Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять Ассоциируете свой токен + Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. @@ -690,6 +693,7 @@ Карта уже подписывала транзакции Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше Нравится Tangem? + Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его Необходима плата за аренду сети %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. Недостаточно %1$s для оплаты комиссии сети diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1b3c413136..2fb4ea6a93 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -553,10 +553,12 @@ 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. + 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 %s transaction is complete + Selling %s is not available at the moment. Please check our updates. Choose address Generate XPUB Hide @@ -668,9 +670,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 + Hot 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. @@ -688,6 +691,7 @@ 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 diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index e2abfa0f4b..961dc949c5 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/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt new file mode 100644 index 0000000000..c43611f1c4 --- /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.material.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..1cff53accd --- /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.material.SnackbarHostState +import androidx.compose.material.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..a8bc0cdfae --- /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>, +) : 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/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/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/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/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/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 43d5796d15..6211aace4f 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.DefaultSwapPromoRepository -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.SwapPromoRepository -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/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..f15c13019f 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,12 @@ 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.core.utils.lceError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -55,6 +59,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, @@ -215,6 +223,30 @@ internal class DefaultCurrenciesRepository( .cancellable() } + override fun getMultiCurrencyWalletCurrenciesUpdatesLce( + userWalletId: UserWalletId, + ): LceFlow> = lceFlow { + val userWallet = getUserWallet(userWalletId) + catch({ ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) }) { + raise(it.lceError()) + } + + launch(dispatchers.io) { + combine( + getMultiCurrencyWalletCurrencies(userWallet), + isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } }, + ) { currencies, isFetching -> + send(currencies, isStillLoading = isFetching) + }.collect() + } + + withContext(dispatchers.io) { + catch({ fetchTokensIfCacheExpired(userWallet, refresh = false) }) { + raise(it.lceError()) + } + } + } + override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, @@ -369,11 +401,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/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 f25a65291c..7f1ae0f462 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,6 +1,8 @@ package com.tangem.data.tokens.repository +import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain +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 @@ -8,8 +10,10 @@ 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.core.utils.lceError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -20,10 +24,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") @@ -41,6 +42,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, @@ -56,6 +61,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.lceError()) + } + } + } + override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { val currencies = getCurrencies(userWalletId, networks) withContext(dispatchers.io) { @@ -82,15 +107,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/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..3b4bfb61e0 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,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.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 timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken 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..38f09a5623 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,8 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +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 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..aa21c01410 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 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..6797014fd2 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { /** Domain */ implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) 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/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/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/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..65ef8788fa --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -0,0 +1,96 @@ +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], + * `null` if it's a [Lce.Error]. + * + * @return The content of this [Lce] or `null`. + */ + fun getOrNull(): C? = fold( + ifLoading = ::identity, + 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..3a5715abf5 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -0,0 +1,99 @@ +package com.tangem.domain.core.lce + +import arrow.core.raise.Raise +import com.tangem.domain.core.utils.lceContent +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: LceRaise.(C) -> Lce, +) : Raise> by 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: Lce): Nothing { + scope.launch(NonCancellable) { + scope.send(r) + scope.close() + } + + raise.raise(r) + } + + /** + * 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(raise, content) + } else { + content.lceContent() + } + + 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 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 [LceFlowScope] context. + * @return A [LceFlow] representing the result of the [block]. + */ +@OptIn(ExperimentalTypeInference::class) +fun lceFlow( + ifLoading: LceRaise.(C) -> Lce = { 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..22a94b519b --- /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(ifLoading: (partialContent: C?) -> C = { raise(lceLoading()) }): C = when (this) { + is Lce.Loading -> { + isLoading.set(true) + + ifLoading(partialContent) + } + 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/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index bc0e716a7a..f21bb97aa5 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(project(":core:utils")) implementation(project(":common")) implementation(project(":libs:auth")) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens.models) 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/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 +214,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) + } }, ), ) 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..ba58967107 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 @@ -2,7 +2,7 @@ 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.datasource.asset.reader.AssetReader import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.utils.converter.Converter import com.tangem.blockchain.common.txhistory.TransactionHistoryItem as SdkTransactionHistoryItem 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..29ea70a4cb 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 @@ -4,7 +4,7 @@ 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.datasource.asset.reader.AssetReader import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.SmartContractMethod import com.tangem.utils.converter.Converter @@ -40,6 +40,7 @@ class SdkTransactionTypeConverter( } } + @Deprecated(message = "Use AssetReader instead") private fun readSmartContractMethods(): Map { val json = assetReader.readJson("contract_methods") return adapter.fromJson(json) ?: emptyMap() 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..b6620652d5 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 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/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 5d91ab4124..c0a73ff9fd 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -11,9 +11,10 @@ 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.wallets.models) 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/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/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..74e89db52c 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 @@ -9,13 +9,10 @@ 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.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] @@ -29,7 +26,6 @@ class GetCryptoCurrencyActionsUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, - private val sendFeatureToggles: SendFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -73,17 +69,18 @@ class GetCryptoCurrencyActionsUseCase( walletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, states = createListOfActions( - userWallet, - coinStatus, - cryptoCurrencyStatus, + userWallet = userWallet, + coinStatus = coinStatus, + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) } /** * 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?, @@ -91,7 +88,7 @@ class GetCryptoCurrencyActionsUseCase( ): 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) @@ -102,52 +99,91 @@ 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)) + activeList.add(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)) } // 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 } @@ -155,62 +191,52 @@ class GetCryptoCurrencyActionsUseCase( private fun getActionsForUnreachableCurrency( cryptoCurrencyStatus: CryptoCurrencyStatus, ): 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)) + actionsList.add(TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None)) } - 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, + cryptoCurrencySymbol = coinStatus?.currency?.symbol.orEmpty(), + ) + } + else -> { + ScenarioUnavailabilityReason.None } } } @@ -218,8 +244,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/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/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt new file mode 100644 index 0000000000..27315e15a7 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.tokens.model + +sealed class ScenarioUnavailabilityReason { + data object None : ScenarioUnavailabilityReason() + + // send&sell-specific + data class PendingTransaction( + val withdrawalScenario: WithdrawalScenario, + val cryptoCurrencySymbol: 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() + + 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/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt new file mode 100644 index 0000000000..c358cd158e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -0,0 +1,153 @@ +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.flow.* + +internal class CurrenciesStatusesLceOperations( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, +) { + + fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { + return getMultiCurrencyWalletCurrencies(userWalletId).transform 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 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 + } + + 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..4ae1a1f01c 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 -> 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/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 612140fbfb..9c75a10432 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,7 +4,6 @@ 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.repository.CurrenciesRepository @@ -18,16 +17,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(), 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..61cc6f66ee 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,7 +57,7 @@ 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) @@ -65,7 +67,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 getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency @@ -75,7 +77,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 +89,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 +104,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 +128,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 +142,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 +165,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 +175,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 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 6ec3fbb11a..63c61d02bd 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.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId @@ -20,6 +21,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 * 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/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 6fb77065d5..e3233d8474 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 @@ -78,6 +80,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, 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 ecf1d2a23b..9cf17f4e9e 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 @@ -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.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId @@ -21,6 +23,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/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/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/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/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/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/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index 2b43096bf4..59084184cc 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 @@ -6,27 +6,21 @@ 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 * - * @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 { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { DeleteWalletError.DataError }, - ) - userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) .doOnSuccess { return Unit.right() } .doOnFailure { return DeleteWalletError.UnableToDelete.left() } 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..5d3d60350f 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,21 +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() ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { 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 3c589f1bcd..76872f50ff 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,20 +1,18 @@ 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 /** * 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> { - return requireNotNull(walletsStateHolder.userWalletsListManager).userWallets - } + operator fun invoke(): Flow> = userWalletsListManager.userWallets } \ 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/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 8e25c40c1e..0f3becefc0 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 { 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/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/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/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 587cb6d67c..43a930c199 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -58,6 +58,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/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 4b30a9466f..0cdc5ba539 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 @@ -3,6 +3,8 @@ package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain 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 @@ -248,6 +250,13 @@ internal class SendNotificationFactory( } } + private fun MutableList.addFeeCoverageNotification(sendingAmount: Boolean) { + if (sendingAmount) { + analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage) + add(SendNotification.Warning.FeeCoverageNotification) + } + } + private fun MutableList.addHighFeeWarningNotification( sendAmount: BigDecimal, ignoreAmountReduce: Boolean, @@ -275,6 +284,21 @@ internal class SendNotificationFactory( } } + // todo remove in [REDACTED_TASK_KEY] + private fun MutableList.addMinimumAmountErrorNotification( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + ) { + val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider() + val minimum = BigDecimal(DOGECOIN_MINIMUM) + + val isDogecoin = isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value) + val isExceedDustLimit = checkDustLimits(feeAmount, receivedAmount, minimum) + if (isDogecoin && isExceedDustLimit) { + add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM)) + } + } + private suspend fun MutableList.addDustWarningNotification( feeAmount: BigDecimal, receivedAmount: BigDecimal, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index b06d42a23d..c0be8a0756 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.fee +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero import com.tangem.core.ui.utils.parseBigDecimal diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 8f7a69c8c1..c77f28309b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.send.impl.presentation.state.fields.SendTextField import kotlinx.coroutines.delay +import kotlinx.coroutines.job @Composable internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) { 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 5410b54f17..72e1de02e6 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 @@ -557,6 +557,7 @@ internal class SendViewModel @Inject constructor( ) return true } + val feeSelectorState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false return checkIfFeeTooHigh( feeSelectorState = feeSelectorState, onShow = { diff -> 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..68c61f2c71 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,6 +8,7 @@ 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 @@ -20,11 +21,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 +48,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 +249,7 @@ internal class DefaultSwapRepository @Inject constructor( fromContractAddress: String, fromNetwork: String, toContractAddress: String, + fromAddress: String, toNetwork: String, fromAmount: String, fromDecimals: Int, @@ -266,6 +267,7 @@ internal class DefaultSwapRepository @Inject constructor( fromContractAddress = fromContractAddress, fromNetwork = fromNetwork, toContractAddress = toContractAddress, + fromAddress = fromAddress, toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, @@ -389,8 +391,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/converters/LeastTokenInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt index 4ee7532cc7..ad6e5fb457 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt @@ -1,8 +1,8 @@ package com.tangem.feature.swap.converters import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.express.models.request.LeastTokenInfo -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.converter.Converter diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 95b8469b2a..98881e9a09 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,7 +8,7 @@ import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.converters.ErrorsDataConverter @@ -33,7 +33,7 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - walletsStateHolder: WalletsStateHolder, + userWalletsListManager: UserWalletsListManager, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, ): SwapRepository { @@ -42,7 +42,7 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - walletsStateHolder = walletsStateHolder, + userWalletsListManager = userWalletsListManager, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index 0a04d8950f..6ae1d7e0d1 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -58,6 +58,7 @@ interface SwapRepository { fromContractAddress: String, fromNetwork: String, toContractAddress: String, + fromAddress: String, toNetwork: String, fromAmount: String, fromDecimals: Int, diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index e0cb9a413a..10ccde5b9f 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) 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 0b77da443d..bea16b1e5c 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 @@ -7,10 +7,10 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.minimalAmount 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.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -583,13 +583,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) } @@ -1048,13 +1049,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( 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..fc191d1d2d 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 @@ -13,7 +13,7 @@ import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.TransactionRepository 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 @@ -79,8 +79,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 +106,11 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetCardTokensListUseCase { return GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - dispatchers = dispatchers, ) } 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..74d4f48e01 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, 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 bd84a05cac..2a873d453c 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 @@ -15,7 +15,6 @@ data class UiActions( val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, val onReduceAmount: (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/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c3fd4f4fd5..ec0eeb5e66 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, ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus @@ -316,10 +314,9 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, - ignoreAmountReduce: Boolean, ): List { val warnings = mutableListOf() - maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) + maybeAddDomainWarnings(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) @@ -355,11 +352,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 -> { @@ -397,23 +390,20 @@ internal class StateBuilder( ) } is Warning.ReduceAmountWarning -> { - if (!ignoreAmountReduce) { - warnings.add( - SwapWarning.ReduceAmount( - notificationConfig = createReduceAmountNotificationConfig( - 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( + amount = it.tezosFeeThreshold.toPlainString(), + onConfirmClick = { + val fromAmount = quoteModel.fromTokenInfo.tokenAmount + val patchedAmount = fromAmount.copy( + value = fromAmount.value - it.tezosFeeThreshold, + ) + actions.onReduceAmount(patchedAmount) + }, ), - ) - } + ), + ) } } } @@ -1353,20 +1343,14 @@ internal class StateBuilder( ) } - private fun createReduceAmountNotificationConfig( - amount: String, - onConfirmClick: () -> Unit, - onDismissClick: () -> Unit, - ): NotificationConfig { + private fun createReduceAmountNotificationConfig(amount: String, onConfirmClick: () -> Unit): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.send_notification_high_fee_title), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(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, ), ) } 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..f0441ea811 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 @@ -497,7 +497,6 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, - reduceAmountIgnore = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), 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 d9598de96f..e78235321b 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 @@ -881,12 +881,6 @@ internal class SwapViewModel @Inject constructor( }, onMaxAmountSelected = ::onMaxAmountClicked, onReduceAmount = ::onReduceAmountClicked, - 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/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 6db0120517..0ec7c8726f 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) 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..9e5149bf68 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 @@ -87,10 +87,10 @@ internal object TokenDetailsPreviewData { ) private val actionButtons = persistentListOf( - TokenDetailsActionButton.Buy(enabled = true, onClick = {}), - TokenDetailsActionButton.Send(enabled = true, onClick = {}), + TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), + TokenDetailsActionButton.Send(dimContent = false, onClick = {}), TokenDetailsActionButton.Receive(onClick = {}), - TokenDetailsActionButton.Swap(enabled = true, onClick = {}), + TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons) 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..85a9c70cf8 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 @@ -14,36 +14,35 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { /** * 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 */ data class Receive(override val onClick: () -> Unit) : TokenDetailsActionButton( @@ -58,30 +57,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..82bb4bbd12 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? @@ -77,5 +77,22 @@ 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, + ) + } } } \ 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..24f7f0c2a0 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,33 @@ 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) }, + ) } 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/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 5048215ac6..05221db9ec 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,7 +130,7 @@ 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( @@ -140,7 +140,7 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - 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 eaeb7f760a..d88f65365c 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,7 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 163a1516ae..575565370e 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 @@ -88,11 +88,11 @@ internal class TokenDetailsSkeletonStateConverter( private fun createButtons(): ImmutableList { return persistentListOf( - TokenDetailsActionButton.Buy(enabled = false, onClick = {}), - TokenDetailsActionButton.Send(enabled = false, onClick = {}), + TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), + TokenDetailsActionButton.Send(dimContent = false, onClick = {}), TokenDetailsActionButton.Receive(onClick = {}), - TokenDetailsActionButton.Sell(enabled = false, onClick = {}), - TokenDetailsActionButton.Swap(enabled = false, onClick = {}), + 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..0109f66fa5 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 @@ -10,16 +10,12 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState 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.* import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver 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 @@ -39,8 +35,9 @@ import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import java.lang.IllegalArgumentException -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, @@ -177,6 +174,19 @@ 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 getRefreshingState(): TokenDetailsState { return refreshStateConverter.convert(true) } @@ -321,4 +331,57 @@ 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.warning_send_blocked_pending_transactions_message, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencySymbol), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencySymbol), + ) + } + } + 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.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/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/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 712afdeb80..8b290061ed 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,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.CryptoCurrency @Suppress("TooManyFunctions") @@ -8,13 +9,15 @@ 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 +27,6 @@ interface TokenDetailsClickIntents { fun onRefreshSwipe() - fun onBuyClick() - fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) fun onReloadClick() 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..26d69cf20d 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 @@ -25,6 +25,7 @@ import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase 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.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -372,9 +373,11 @@ 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 @@ -401,9 +404,11 @@ internal class TokenDetailsViewModel @Inject constructor( 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) } @@ -469,9 +474,11 @@ internal class TokenDetailsViewModel @Inject constructor( } } - override fun onReceiveClick() { + override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return + if (handleUnavailabilityReason(unavailabilityReason)) return + viewModelScope.launch(dispatchers.io) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened) @@ -507,9 +514,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,9 +531,11 @@ 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)) } @@ -681,7 +692,15 @@ internal class TokenDetailsViewModel @Inject constructor( shouldShowSwapPromoTokenUseCase.neverToShow() analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) } - onSwapClick() + onSwapClick(ScenarioUnavailabilityReason.None) + } + + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { + if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false + + uiState = 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..301b8984f8 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) 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/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5342e0ece5..068801b384 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,14 @@ 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.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 +41,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 +68,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 +92,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 +102,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 +118,7 @@ internal class OrganizeTokensViewModel @Inject constructor( ifLeft = stateHolder::updateStateWithError, ifRight = { stateHolder.updateStateAfterTokenListSorting(it) - tokenList = it + cachedTokenList = it }, ) } @@ -138,7 +141,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 +164,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 TokenList.FiatBalance.Loading + } + + maybeTokenList.getOrElse { error -> + stateHolder.updateStateWithError(error) + + null + } } } @@ -189,7 +217,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/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 655a49a92b..23f6a0c60d 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,12 +2,12 @@ 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 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 7432cbb84e..51fbfe6742 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 promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) } return combine( - flow = getTokenListUseCase(userWallet.walletId).conflate(), + flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = shouldShowSwapPromoWalletUseCase().conflate(), 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..34afb01364 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 @@ -28,7 +28,7 @@ internal sealed interface WalletAlertState { } data class DefaultAlert( - override val title: TextReference, + override val title: TextReference?, override val message: TextReference, override val onConfirmClick: (() -> Unit)?, ) : Basic() 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..8035434bcf 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,6 +18,9 @@ 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 @@ -25,29 +28,40 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { * 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, ), ) @@ -56,12 +70,16 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { * * @property onClick lambda be invoked when Receive button is 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, + ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) @@ -69,14 +87,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 +107,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/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 73065cb197..82097668ff 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 = {}), + 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..ef747e1305 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,47 @@ 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) + }, ) } 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/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..34579188d3 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,7 +44,7 @@ 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( @@ -54,13 +54,13 @@ internal class SingleWalletMarketPriceConverter( ) } - 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/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 866f9981a0..2dc37ce327 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( @@ -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 = {}), + 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..cf4c25c45f 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,17 @@ 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 -> + maybeContent ?: 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 +83,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 +98,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..62576cf73a 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,8 +1,9 @@ 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 @@ -10,16 +11,19 @@ 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.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,16 +40,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 } + override suspend fun onTokenListReceived(maybeTokenList: Lce) { + updateSortingIfNeeded(maybeTokenList) + } + + private suspend fun updateSortingIfNeeded(maybeTokenList: Lce) { + val tokenList = maybeTokenList.getOrNull() ?: return if (!checkNeedSorting(tokenList)) return applyTokenListSortingUseCase( 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/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 9819f09790..cfeccc9db0 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 @@ -22,6 +22,7 @@ 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.unit.Dp @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.TestTags import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder @@ -133,7 +135,7 @@ private fun WalletContent( .padding(horizontal = horizontalPadding) LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().testTag(TestTags.WALLET_SCREEN), contentPadding = PaddingValues( top = TangemTheme.dimens.spacing8, bottom = TangemTheme.dimens.spacing92, 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..e74ecbdb0b 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 @@ -7,11 +7,9 @@ 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.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler @@ -37,7 +35,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -56,7 +53,6 @@ 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, @@ -141,16 +137,6 @@ 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) } 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..3294411975 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 @@ -7,8 +7,7 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot 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.WrappedList -import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.* import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver @@ -19,6 +18,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 @@ -39,11 +39,18 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import java.lang.IllegalArgumentException 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) @@ -53,12 +60,6 @@ interface WalletCurrencyActionsClickIntents { fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - - fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - - fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onExploreClick() } @@ -82,13 +83,18 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, ) : 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 +127,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun sendToken( cryptoCurrency: CryptoCurrency.Token, - cryptoCurrencyStatus: CryptoCurrencyStatus.Status, + cryptoCurrencyStatus: CryptoCurrencyStatus.Value, feeCurrencyStatus: CryptoCurrencyStatus?, userWallet: UserWallet, ) { @@ -274,11 +280,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 +302,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 +327,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 +423,77 @@ 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.warning_send_blocked_pending_transactions_message, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencySymbol), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencySymbol), + ) + } + } + 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.None -> { + throw IllegalArgumentException("The unavailability reason must be other than None") + } + } + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 80bda90466..99fc8ec14e 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,12 +81,14 @@ swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" leakcanary = "2.13" +decompose = "2.2.2" +room = "2.6.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.10-618" +tangemBlockchainSdk = "develop-620" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.9-343" +tangemCardSdk = "develop-351" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -120,6 +121,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 @@ -197,11 +199,12 @@ test-kaspresso = { module = "com.kaspersky.android-components:kaspresso", versio 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" } @@ -221,6 +224,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" } @@ -249,4 +253,9 @@ 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" } # 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/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..88d96a0405 --- /dev/null +++ b/libs/blockchain-sdk/build.gradle.kts @@ -0,0 +1,45 @@ +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.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 +} \ 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..406be2d877 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -0,0 +1,102 @@ +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.* +import kotlinx.coroutines.flow.* +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 walletManagerFactory: Flow by lazy(::createWalletManagerFactory) + + private val mainScope = CoroutineScope(dispatchers.main) + + 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(), + 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..feae18a4e4 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -0,0 +1,37 @@ +package com.tangem.blockchainsdk + +import com.tangem.blockchain.common.AccountCreator +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 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, +) { + + fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { + Timber.d("Create WalletManagerFactory") + + return WalletManagerFactory( + config = config, + blockchainProviderTypes = blockchainProviderTypes, + accountCreator = accountCreator, + 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..2a2e2a42f9 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt @@ -0,0 +1,64 @@ +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.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(blockchain = blockchain, name = provider.name) + ProviderModel.UnsupportedType -> { + Timber.e("$blockchain provider type is not supported") + null + } + } + } + + blockchain to providerTypes + } + .toMap() + } + + @Suppress("CyclomaticComplexMethod") + private fun createPrivateProviderType(blockchain: Blockchain, 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 + else -> { + Timber.e("$blockchain private provider ($name) is not supported") + null + } + } + } +} \ 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..ff8c190e0e --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -0,0 +1,86 @@ +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, + ) + } + + 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..3ebbe75b92 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -0,0 +1,59 @@ +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.loader.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.store.DefaultRuntimeStore +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, + ): WalletManagerFactoryCreator { + return WalletManagerFactoryCreator( + accountCreator = DefaultAccountCreator(authProvider, tangemTechApi), + blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), + blockchainSDKLogger = blockchainSDKLogger, + ) + } +} \ 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..310ec39f9a --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt @@ -0,0 +1,97 @@ +package com.tangem.blockchainsdk.loader + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.asset.loader.AssetLoader +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 authProvider auth provider + * @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 authProvider: AuthProvider, + 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(): BlockchainProvidersResponse { + return tangemTechServiceApi.getBlockchainProviders( + cardPublicKey = authProvider.getCardPublicKey(), + cardId = authProvider.getCardId(), + ) + } + + /** 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 result = local + remote.filterValues { it.isNotEmpty() } + + if (result != remote) { + val missingBlockchains = result.keys - remote.keys + val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys + + recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) + } + + return result + } + + 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) + } + + private companion object { + const val PROVIDER_TYPES_FILE_NAME = "tangem-app-config/providers_order" + } +} \ 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 97% 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 dc608f1ae4..1ae43f5950 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 @@ -114,6 +114,8 @@ 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 else -> null } } @@ -229,6 +231,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.TaraxaTestnet -> "taraxa/test" Blockchain.Base -> "base" Blockchain.BaseTestnet -> "base/test" + Blockchain.Koinos -> "koinos" + Blockchain.KoinosTestnet -> "koinos/test" } } @@ -302,6 +306,7 @@ 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" } } @@ -335,4 +340,6 @@ private val excludedBlockchains = listOf( Blockchain.MantaTestnet, Blockchain.Mantle, Blockchain.MantleTestnet, + Blockchain.Koinos, + Blockchain.KoinosTestnet, ) \ No newline at end of file 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/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 8c94ba65a8..e297edfac7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,11 +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 - maven("https://nexus.tangem-tech.com/repository/maven-releases/") + 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") @@ -36,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 @@ -44,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/") @@ -72,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 @@ -150,7 +192,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")