diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3682efb9f5..a4c7cd0cfd 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) @@ -216,9 +216,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..7f38b804c2 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -0,0 +1,57 @@ +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.rules.ActivityScenarioRule +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/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..d547bbdf53 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,7 +36,7 @@ () 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 +221,7 @@ 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) initIntentHandlers() @@ -264,9 +268,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 } } @@ -437,15 +441,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 71% rename from app/src/main/java/com/tangem/tap/TapApplication.kt rename to app/src/main/java/com/tangem/tap/TangemApplication.kt index 7c26fe7570..690919b008 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,11 +34,11 @@ 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 @@ -61,134 +58,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 +192,12 @@ 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() - } + store.dispatch(GlobalAction.UpdateUserWalletsListManager(generalUserWalletsListManager)) } val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) @@ -264,16 +243,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, ), ), ) @@ -373,14 +350,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/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/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index fbe3e38b89..b3e337e2bf 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 @@ -95,17 +95,10 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde ) } is GlobalAction.UpdateUserWalletsListManager -> { - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) + val generalUserWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - 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) - } + appStateHolder.userWalletsListManager = generalUserWalletsListManager + globalState.copy(userWalletsListManager = generalUserWalletsListManager) } is GlobalAction.ChangeAppThemeMode -> globalState.copy( appThemeMode = action.appThemeMode, 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/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/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 084134986d..6d9b682e61 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 @@ -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 { 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..be433d70bb 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 @@ -55,9 +55,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 +65,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 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..a24becfb81 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,15 +1,11 @@ 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 @@ -28,26 +24,18 @@ 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, + mnemonicRepository: MnemonicRepository, + 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/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..407643fb6d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -0,0 +1,98 @@ +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.tap.domain.tasks.product.CreateProductWalletTaskResponse + +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 + + 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 + + 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) +} \ 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 82% 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..b6d9589111 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 @@ -29,6 +29,7 @@ import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.derivation.DeriveWalletPublicKeyTask 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 @@ -40,10 +41,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 +56,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 +88,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 +104,7 @@ class TangemSdkManager( ) } - suspend fun importWallet( + override suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, passphrase: String?, @@ -135,14 +136,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 +154,7 @@ class TangemSdkManager( ) } - suspend fun resetToFactorySettings( + override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { @@ -167,7 +168,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 +178,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 +194,7 @@ class TangemSdkManager( ) } - suspend fun setAccessCode(cardId: String?): CompletionResult { + override suspend fun setAccessCode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, @@ -201,7 +202,7 @@ class TangemSdkManager( ) } - suspend fun setLongTap(cardId: String?): CompletionResult { + override suspend fun setLongTap(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, @@ -209,7 +210,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 +221,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 +233,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 +257,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,11 +266,11 @@ 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 } 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..8279c72670 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -0,0 +1,149 @@ +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.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.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.domain.sdk.mocks.MockProvider +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Suppress("TooManyFunctions") +class MockTangemSdkManager( + private val resources: Resources, +) : TangemSdkManager { + + override val canUseBiometry: Boolean + get() = false + + override val needEnrollBiometrics: Boolean + get() = TODO() + + override val keystoreManager: KeystoreManager + get() = TODO() + + override val secureStorage: SecureStorage + get() = TODO() + + override val userCodeRequestPolicy: UserCodeRequestPolicy + get() = TODO() + + override suspend fun scanProduct( + cardId: String?, + messageRes: Int?, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return CompletionResult.Success(MockProvider.getScanResponse()) + } + + override suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean, + ): CompletionResult { + TODO() + } + + override suspend fun importWallet( + scanResponse: ScanResponse, + mnemonic: String, + passphrase: String?, + shouldReset: Boolean, + ): CompletionResult { + TODO() + } + + override suspend fun derivePublicKeys( + cardId: String?, + derivations: Map>, + ): CompletionResult { + TODO() + } + + override suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult { + TODO() + } + + override suspend fun resetToFactorySettings( + cardId: String, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + TODO() + } + + override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + TODO() + } + + override suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { + TODO() + } + + override suspend fun clearSavedUserCodes(): CompletionResult { + TODO() + } + + override suspend fun setPasscode(cardId: String?): CompletionResult { + TODO() + } + + override suspend fun setAccessCode(cardId: String?): CompletionResult { + TODO() + } + + override suspend fun setLongTap(cardId: String?): CompletionResult { + TODO() + } + + override suspend fun setAccessCodeRecoveryEnabled( + cardId: String?, + enabled: Boolean, + ): CompletionResult { + TODO() + } + + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + TODO() + } + + override suspend fun runTaskAsync( + runnable: CardSessionRunnable, + cardId: String?, + initialMessage: Message?, + accessCode: String?, + @DrawableRes iconScanRes: Int?, + ): CompletionResult = withContext(Dispatchers.Main) { + TODO() + } + + @Suppress("MagicNumber") + override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { + } + + @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) { + TODO() + } +} \ 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..0d0c1eabed --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.domain.sdk.mocks + +import com.tangem.domain.models.scan.ProductType +import com.tangem.tap.domain.sdk.mocks.wallet.WalletMocks +import com.tangem.tap.domain.sdk.mocks.wallet2.Wallet2Mocks + +object MockProvider { + + var productType: ProductType = ProductType.Wallet + + fun getScanResponse() = getMocks(productType).scanResponse + + private fun getMocks(productType: ProductType): Mocks { + return when (productType) { + ProductType.Wallet -> WalletMocks + ProductType.Wallet2 -> Wallet2Mocks + else -> TODO() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/Mocks.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/Mocks.kt new file mode 100644 index 0000000000..a7de03220a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/Mocks.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.sdk.mocks + +import com.tangem.domain.models.scan.ScanResponse + +interface Mocks { + + val scanResponse: ScanResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/wallet/WalletMocks.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/wallet/WalletMocks.kt new file mode 100644 index 0000000000..2157f6fd60 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/wallet/WalletMocks.kt @@ -0,0 +1,127 @@ +package com.tangem.tap.domain.sdk.mocks.wallet + +import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.EncryptionMode +import com.tangem.common.card.FirmwareVersion +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.tap.domain.sdk.mocks.Mocks +import java.util.Date + +object WalletMocks : Mocks { + + private 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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/wallet2/Wallet2Mocks.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/wallet2/Wallet2Mocks.kt new file mode 100644 index 0000000000..86b865896f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/wallet2/Wallet2Mocks.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.domain.sdk.mocks.wallet2 + +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.domain.sdk.mocks.Mocks + +object Wallet2Mocks : Mocks { + + override val scanResponse: ScanResponse + get() = TODO("Not yet implemented") +} \ 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..e203d7c4d5 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 @@ -88,6 +88,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..16bc4b280d 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 @@ -227,11 +225,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..4fed4bf821 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.WalletsStateHolder 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, + walletsStateHolder: WalletsStateHolder, ): 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, + walletsStateHolder = walletsStateHolder, + 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..d7b74b884b 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.WalletsStateHolder +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 walletsStateHolder: WalletsStateHolder, val blockchainHelper: WcBlockchainHelper, ) { + private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { + GetSelectedWalletUseCase(walletsStateHolder) + } + + 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..39dc386ee6 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 @@ -329,8 +325,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 +366,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 +383,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 +393,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 +410,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 +488,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, 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..f29861dbbc 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,26 @@ 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 +28,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 +47,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 +71,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 +80,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() @@ -402,135 +179,7 @@ class WalletConnectMiddleware { 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/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..2048292209 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,7 +19,6 @@ 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 @@ -77,8 +76,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( 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..ae12f26669 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,7 @@ package com.tangem.tap.features.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController @@ -11,6 +12,7 @@ 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.tap.features.main.model.MainScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -26,6 +28,8 @@ 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 dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -39,7 +43,17 @@ internal class MainViewModel @Inject constructor( val state: StateFlow = stateHolder.stateFlow + var isSplashScreenShown: Boolean = true + private set + init { + viewModelScope.launch(dispatchers.main) { + blockchainSDKFactory.init() + isSplashScreenShown = false + } + + viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } + updateAppCurrencies() observeFlips() displayBalancesHidingStatusToast() 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..33784bac12 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 @@ -79,6 +79,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 +92,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) 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..dcb56e9eb5 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 @@ -36,6 +36,7 @@ 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 @@ -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) 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..cfcd3f41af 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 @@ -562,7 +562,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..563c72cd24 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,7 +7,6 @@ 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 @@ -18,8 +17,6 @@ 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.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -46,16 +43,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 @@ -91,82 +86,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) @@ -188,7 +107,9 @@ internal class SaveWalletMiddleware { 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 +119,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 +139,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 dd10c08389..2922bd5b22 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 acf336dac3..bfd30da35b 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,10 +3,10 @@ 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.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..b59a973b64 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 @@ -165,9 +165,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/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/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index 52beeb8387..d3965fa2cf 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -13,7 +13,7 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.ExchangeService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow 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..32c48ed96d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -21,10 +21,10 @@ 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.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* 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..43c70cab02 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -4,10 +4,10 @@ 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.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.blockchainsdk.utils.toNetworkId 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.domain.walletmanager.WalletManagersFacade 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/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/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index e4239e40d6..06a4d5180e 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -39,6 +39,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,6 +54,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) + mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) 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/tangemTech/TangemTechServiceApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt new file mode 100644 index 0000000000..107db49e49 --- /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("v1/networks") + 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/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt new file mode 100644 index 0000000000..e271679df4 --- /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 is null")) + parsedConfig + }, + onFailure = { + Timber.e(it, "Failed to load config 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 is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config 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 is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config 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..32ea540a36 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 @@ -146,8 +146,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/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 94987bf391..4071534469 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, 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..d229d1dbf1 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,7 @@ 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.TangemTechServiceApi import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers @@ -19,6 +20,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 @@ -96,6 +98,33 @@ class NetworkModule { .create(TangemTechApi::class.java) } + @Provides + @Singleton + fun provideTangemTechServiceApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechServiceApi { + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .baseUrl(PROD_TANGEM_TECH_BASE_URL) + .client( + OkHttpClient.Builder() + .callTimeout(timeout = 5, unit = TimeUnit.SECONDS) + .addHeaders( + CacheControlHeader, + AppVersionPlatformHeaders(appVersionProvider), + // TODO("refactor header init") get auth data after biometric auth to avoid race condition + // AuthenticationHeader(authProvider), + ) + .addLoggers(context) + .build(), + ) + .build() + .create(TangemTechServiceApi::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]" 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/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 84088baede..d5cd06e891 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.0" }, - { - "name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED", - "version": "5.8.0" - }, { "name": "LOCAL_USER_LOGS_ENABLED", "version": "5.8.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 5d25d41f8e..1e87046df0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -540,6 +540,7 @@ Выбранная операция в данный момент недоступна. Попробуйте позже. В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. + Выбранная операция в данный момент недоступна. Попробуйте позже. Обмен %s не доступен. Но мы работаем над его добавлением. В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением. Сгенерировать XPUB diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a099c577d4..f0b93b187c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -537,6 +537,7 @@ 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. + This operation is currently unavailable. Please try again later. %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. Generate XPUB 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/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/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 +216,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..abd618d4b1 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 @@ -73,9 +73,9 @@ class GetCryptoCurrencyActionsUseCase( walletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, states = createListOfActions( - userWallet, - coinStatus, - cryptoCurrencyStatus, + userWallet = userWallet, + coinStatus = coinStatus, + cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) } @@ -91,7 +91,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 +102,61 @@ 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( + userWalletId = userWallet.walletId, + 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)) + 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)) + activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Sell(false)) + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = ScenarioUnavailabilityReason.SellUnavailable(cryptoCurrency.name), + ), + ) } // hide - activeList.add(TokenActionsState.ActionState.HideToken(true)) + activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return activeList + disabledList } @@ -155,63 +164,140 @@ 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.NoQuotes)) + actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.NoQuotes)) + actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.NoQuotes)) 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( + private suspend fun getSendUnavailabilityReason( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, - ): Boolean { - val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrencyStatus.currency) - val notEnoughBalanceForFee = isNotEnoughBalanceForFee( - feePaidCurrency = feePaidCurrency, + ): ScenarioUnavailabilityReason { + val insufficientFundsForFee = insufficientFundsForFee( + userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, coinStatus = coinStatus, ) - return cryptoCurrencyStatus.value.amount.isNullOrZero() || - notEnoughBalanceForFee || + + return when { + cryptoCurrencyStatus.value.amount.isNullOrZero() -> { + ScenarioUnavailabilityReason.EmptyBalance + } + insufficientFundsForFee != null -> { + ScenarioUnavailabilityReason.InsufficientFundsForFee( + currencyName = insufficientFundsForFee.currencyName, + networkName = insufficientFundsForFee.networkName, + feeCurrencyName = insufficientFundsForFee.feeCurrencyName, + feeCurrencySymbol = insufficientFundsForFee.feeCurrencySymbol, + ) + } currenciesRepository.hasPendingTransactions( cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus, - ) + ) -> { + ScenarioUnavailabilityReason.PendingTransaction(coinStatus?.currency?.symbol.orEmpty()) + } + else -> { + ScenarioUnavailabilityReason.None + } + } } - private fun isNotEnoughBalanceForFee( - feePaidCurrency: FeePaidCurrency, + private suspend fun insufficientFundsForFee( + userWalletId: UserWalletId, 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() + ): FeeInfo? { + val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency) + coinStatus ?: return null + return when { + sendFeatureToggles.isRedesignedSendEnabled && !tokenStatus.value.amount.isZero() -> { + null + } + feePaidCurrency is FeePaidCurrency.Coin && + !tokenStatus.value.amount.isZero() && + coinStatus.value.amount.isZero() -> { + FeeInfo( + currencyName = tokenStatus.currency.name, + networkName = coinStatus.currency.network.name, + feeCurrencyName = coinStatus.currency.name, + feeCurrencySymbol = coinStatus.currency.symbol, + ) + } + feePaidCurrency is FeePaidCurrency.SameCurrency && !tokenStatus.value.amount.isZero() -> { + FeeInfo( + currencyName = tokenStatus.currency.name, + networkName = coinStatus.currency.network.name, + feeCurrencyName = coinStatus.currency.name, + feeCurrencySymbol = coinStatus.currency.symbol, + ) + } + feePaidCurrency is FeePaidCurrency.Token -> { + val feePaidTokenBalance = feePaidCurrency.balance + val amount = tokenStatus.value.amount ?: return null + if (!amount.isZero() && feePaidTokenBalance.isZero()) { + constructTokenBalanceNotEnoughWarning( + userWalletId = userWalletId, + tokenStatus = tokenStatus, + feePaidToken = feePaidCurrency, + ) + } else { + null } } + else -> null + } + } + + private suspend fun constructTokenBalanceNotEnoughWarning( + userWalletId: UserWalletId, + tokenStatus: CryptoCurrencyStatus, + feePaidToken: FeePaidCurrency.Token, + ): FeeInfo { + val token = currenciesRepository + .getMultiCurrencyWalletCurrenciesSync(userWalletId) + .find { + it is CryptoCurrency.Token && + it.contractAddress.equals(feePaidToken.contractAddress, ignoreCase = true) && + it.network.derivationPath == tokenStatus.currency.network.derivationPath + } + return if (token != null) { + FeeInfo( + currencyName = tokenStatus.currency.name, + networkName = token.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) + } else { + FeeInfo( + currencyName = tokenStatus.currency.name, + networkName = tokenStatus.currency.network.name, + feeCurrencyName = feePaidToken.name, + feeCurrencySymbol = feePaidToken.symbol, + ) } } @@ -222,4 +308,11 @@ class GetCryptoCurrencyActionsUseCase( private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } + + data class FeeInfo( + val currencyName: String, + val networkName: String, + val feeCurrencyName: String, + val feeCurrencySymbol: String, + ) } \ 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..a3b36c4216 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.tokens.model + +sealed class ScenarioUnavailabilityReason { + data object None : ScenarioUnavailabilityReason() + + // send-specific + data class PendingTransaction(val cryptoCurrencySymbol: String) : ScenarioUnavailabilityReason() + data object EmptyBalance : ScenarioUnavailabilityReason() + data class InsufficientFundsForFee( + val currencyName: String, + val networkName: String, + val feeCurrencyName: String, + val feeCurrencySymbol: String, + ) : ScenarioUnavailabilityReason() + + // buy-specific + data class BuyUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + // swap-specific + data class NotExchangeable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + // sell-specific + data class SellUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + data object NoQuotes : ScenarioUnavailabilityReason() +} \ 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 fac9f7fc95..3e6005ebc7 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/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 index 7aa875c6af..37f6644beb 100644 --- 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 @@ -2,6 +2,8 @@ package com.tangem.domain.wallets.legacy import kotlinx.coroutines.flow.Flow +// TODO: will be remove in this task [REDACTED_JIRA] +@Deprecated(message = "Provide UserWalletsListManager using DI", level = DeprecationLevel.WARNING) interface WalletsStateHolder { val userWalletsListManager: UserWalletsListManager? 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 0d147eb6fd..e71cdf4d14 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,12 +3,12 @@ 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 import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus 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..783ba7da25 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,7 +21,6 @@ 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 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/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..6ab50afce3 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 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 196d529e7b..f7ccce976e 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 @@ -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/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..cd21841371 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,60 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } + + private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { + return when (unavailabilityReason) { + // send + is ScenarioUnavailabilityReason.PendingTransaction -> { + resourceReference( + id = R.string.warning_send_blocked_pending_transactions_message, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencySymbol), + ) + } + ScenarioUnavailabilityReason.EmptyBalance -> { + resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance, + ) + } + is ScenarioUnavailabilityReason.InsufficientFundsForFee -> { + resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList( + unavailabilityReason.currencyName, + unavailabilityReason.networkName, + unavailabilityReason.currencyName, + unavailabilityReason.feeCurrencyName, + unavailabilityReason.feeCurrencySymbol, + ), + ) + } + 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.SellUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.NoQuotes -> { + resourceReference( + id = R.string.token_button_unavailability_reason_no_quotes, + ) + } + + 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 f4c38aca5c..5722777eb6 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 @@ -26,6 +26,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 @@ -388,9 +389,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 @@ -417,9 +420,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) } @@ -485,9 +490,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) @@ -523,9 +530,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 @@ -538,9 +547,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)) } @@ -697,7 +708,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 63db20714d..eb6f9dc8b2 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..16fa32b0e7 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,79 @@ 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) { + // send + is ScenarioUnavailabilityReason.PendingTransaction -> { + resourceReference( + id = R.string.warning_send_blocked_pending_transactions_message, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencySymbol), + ) + } + ScenarioUnavailabilityReason.EmptyBalance -> { + resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance, + ) + } + is ScenarioUnavailabilityReason.InsufficientFundsForFee -> { + resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList( + unavailabilityReason.currencyName, + unavailabilityReason.networkName, + unavailabilityReason.currencyName, + unavailabilityReason.feeCurrencyName, + unavailabilityReason.feeCurrencySymbol, + ), + ) + } + 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.SellUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.NoQuotes -> { + resourceReference( + id = R.string.token_button_unavailability_reason_no_quotes, + ) + } + 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 56d6351be9..c58b88a4a5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -68,7 +68,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 +82,13 @@ swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" leakcanary = "2.13" +decompose = "2.2.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.9-600" +tangemBlockchainSdk = "develop-601" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.9-343" +tangemCardSdk = "develop-345" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -197,6 +198,8 @@ 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 @@ -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,6 @@ 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" } # endregion Other 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..ecf2e9a795 --- /dev/null +++ b/libs/blockchain-sdk/build.gradle.kts @@ -0,0 +1,39 @@ +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 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..7613213ba4 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt @@ -0,0 +1,57 @@ +package com.tangem.blockchainsdk.loader + +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, +) { + + /** Load [BlockchainProvidersResponse] */ + suspend fun load(): BlockchainProvidersResponse? { + return runCatching(dispatcher = dispatchers.io, block = ::loadRemote) + .fold( + onSuccess = { it }, + onFailure = { + Timber.e(it, "Failed to load blockchain provider types from backend") + loadLocal() + }, + ) + } + + private suspend fun loadRemote(): BlockchainProvidersResponse { + return tangemTechServiceApi.getBlockchainProviders( + cardPublicKey = authProvider.getCardPublicKey(), + cardId = authProvider.getCardId(), + ) + } + + private suspend fun loadLocal(): BlockchainProvidersResponse? { + return assetLoader.load(fileName = PROVIDER_TYPES_FILE_NAME) + } + + 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 36caa85c7f..502c011f5f 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" } } @@ -331,4 +336,6 @@ private val excludedBlockchains = listOf( Blockchain.Nexa, Blockchain.NexaTestnet, Blockchain.Radiant, + Blockchain.Koinos, + Blockchain.KoinosTestnet, ) \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index f288d89a7d..885f32c1d7 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -37,7 +37,7 @@ private fun AppExtension.configureDefaultConfig(project: Project) { buildFeatures.buildConfig = true - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.tangem.common.HiltTestRunner" } } @@ -76,6 +76,7 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil } BuildType.Internal, BuildType.External, + BuildType.Mocked -> { initWith(extension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt index 66437a1de5..17f61ecdd2 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -15,6 +15,7 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v value = "\"$value\"", ) + // TODO remove class TestActionEnabled(isEnabled: Boolean) : BuildConfigField( type = "Boolean", name = "TEST_ACTION_ENABLED", @@ -32,4 +33,10 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v name = "TESTER_MENU_ENABLED", value = isEnabled.toString(), ) + + class MockDataSource(isEnabled: Boolean) : BuildConfigField( + type = "Boolean", + name = "MOCK_DATA_SOURCE", + value = isEnabled.toString(), + ) } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 767e46ba25..53a1c18637 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -27,6 +27,29 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), + ), + ), + + /** + * Build type for QA and business + * + * Features: + * - Env: dev + * - Signing config: debug + * - Logs + * - Enabled mocked datasource + * */ + Mocked( + id = "mocked", + appIdSuffix = "mocked", + versionSuffix = "mocked", + configFields = listOf( + BuildConfigField.Environment(value = "dev"), + BuildConfigField.TestActionEnabled(isEnabled = false), + BuildConfigField.LogEnabled(isEnabled = true), + BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = true), ), ), @@ -50,6 +73,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), ), ), @@ -70,6 +94,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), ), ), @@ -88,6 +113,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), ), ), } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8c94ba65a8..1a729a943f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -72,11 +72,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 +152,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")